From 338da0f2f96faa3e0b71c7496f8f00342d175557 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 11 Feb 2026 18:50:41 +0100 Subject: [PATCH 01/44] initial commit --- contracts/test/_fixture.js | 32 ++++++++++++++++++++++++++++++++ contracts/utils/addresses.js | 3 +++ 2 files changed, 35 insertions(+) diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 68f8196d01..47c974481a 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -2567,6 +2567,37 @@ async function instantRebaseVaultFixture(tokenName) { return fixture; } +async function supernovaAMOFixure() { + const fixture = await defaultFixture(); + const { ousd, usdc } = fixture; + + const factoryABI = [ + "function createPair(address tokenA, address tokenB) external returns (address pair)", + "function getPair(address tokenA, address tokenB) external view returns (address pair)" + ]; + const factory = await ethers.getContractAt(factoryABI, addresses.mainnet.supernovaPairFactory); + + const pairAddress = await factory.getPair(ousd.address, usdc.address); + let pair; + + if (pairAddress === ethers.constants.AddressZero) { + log("Creating new OUSD/USDC pair..."); + const tx = await factory.createPair(ousd.address, usdc.address); + const receipt = await tx.wait(); + const pairCreatedEvent = receipt.events.find(e => e.event === 'PairCreated'); + const newPairAddress = pairCreatedEvent.args.pair; + } else { + log(`Using existing OUSD/USDC pair: ${pairAddress}`); + } + + // TODO fetch strategy contract from deployment file + // use deploy actions + + fixture.supernovaPair = await ethers.getContractAt("IPair", pairAddress); +} + + + // Unit test cross chain fixture where both contracts are deployed on the same chain for the // purposes of unit testing async function crossChainFixtureUnit() { @@ -3088,4 +3119,5 @@ module.exports = { claimRewardsModuleFixture, crossChainFixtureUnit, crossChainFixture, + supernovaAMOFixure, }; diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index e1b82825c7..746a80010d 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -394,6 +394,9 @@ addresses.mainnet.toConsensus.consolidation = addresses.mainnet.toConsensus.withdrawals = "0x00000961Ef480Eb55e80D19ad83579A64c007002"; +addresses.mainnet.supernovaPairFactory = + "0x5aef44edfc5a7edd30826c724ea12d7be15bdc30"; + // Mainnet Merkl addresses.mainnet.CampaignCreator = "0x8BB4C975Ff3c250e0ceEA271728547f3802B36Fd"; From fcf4a617895da5060be3e8c4c1e3c23aae112e3a Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 11 Feb 2026 23:31:05 +0100 Subject: [PATCH 02/44] add some deployment files --- contracts/contracts/proxies/Proxies.sol | 7 ++ contracts/deploy/deployActions.js | 115 +++++++++++++++++- contracts/deploy/mainnet/174_supernova_AMO.js | 60 +++++++++ contracts/test/_fixture.js | 30 +---- contracts/utils/addresses.js | 4 + 5 files changed, 189 insertions(+), 27 deletions(-) create mode 100644 contracts/deploy/mainnet/174_supernova_AMO.js diff --git a/contracts/contracts/proxies/Proxies.sol b/contracts/contracts/proxies/Proxies.sol index 03227c25a8..3f717bcfb7 100644 --- a/contracts/contracts/proxies/Proxies.sol +++ b/contracts/contracts/proxies/Proxies.sol @@ -328,3 +328,10 @@ contract CompoundingStakingSSVStrategyProxy is contract OUSDMorphoV2StrategyProxy is InitializeGovernedUpgradeabilityProxy { } + +/** + * @notice OETHSupernovaAMOProxy delegates calls to a SonicSwapXAMOStrategy implementation + */ +contract OETHSupernovaAMOProxy is InitializeGovernedUpgradeabilityProxy { + +} diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index 7203da9f6a..73f23feafd 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -30,7 +30,7 @@ const { const { metapoolLPCRVPid } = require("../utils/constants"); const { replaceContractAt } = require("../utils/hardhat"); const { resolveContract } = require("../utils/resolvers"); -const { impersonateAccount, getSigner } = require("../utils/signers"); +const { impersonateAccount, impersonateAndFund, getSigner } = require("../utils/signers"); const { getDefenderSigner } = require("../utils/signersNoHardhat"); const { getTxOpts } = require("../utils/tx"); const createxAbi = require("../abi/createx.json"); @@ -1640,6 +1640,117 @@ const deploySonicSwapXAMOStrategyImplementation = async () => { return cSonicSwapXAMOStrategy; }; +// TODO: once this gets deployed on the mainnet delete this function +const deployOETHSupernovaAMOStrategyPoolAndGauge = async () => { + // Account authorized to call createPair on the factory contract + const pairBootstrapper = "0x7F8f2B6D0b0AaE8e95221Ce90B5C26B128C1Cb66"; + const topkenHandlerWhitelister = "0xD09A1388F0CcE25DA97E8bBAbf5D083E25a5Fbc6"; + const sPairBootstrapper = await impersonateAndFund(pairBootstrapper); + const sTokenHandlerWhitelister = await impersonateAndFund(topkenHandlerWhitelister); + + const oeth = await ethers.getContract("OETHProxy"); + + const factoryABI = [ + "function createPair(address tokenA, address tokenB, bool stable) external returns (address pair)", + "event PairCreated(address token0, address token1, bool stable, address pair, uint);" + ]; + const tokenHandlerABI = [ + "function whitelistToken(address _token) external", + "function whitelistConnector(address _token) external", + ]; + const pairCreatedTopic = "0xc4805696c66d7cf352fc1d6bb633ad5ee82f6cb577c453024b6e0eb8306c6fc9"; + + const gaugeManagerAbi = [ + "function createGauge(address _pool, uint256 _gaugeType) external returns (address _gauge, address _internal_bribe, address _external_bribe)", + "function tokenHandler() external view returns (address)", + "event GaugeCreated(address gauge, address creator, address internal_bribe, address external_bribe, address pool)" + ]; + const gaugeManager = await ethers.getContractAt(gaugeManagerAbi, addresses.mainnet.supernovaGaugeManager); + const tokenHandler = await ethers.getContractAt(tokenHandlerABI, await gaugeManager.tokenHandler()); + + const factory = await ethers.getContractAt(factoryABI, addresses.mainnet.supernovaPairFactory); + + let poolAddress; + log("Creating new OETH/WETH pair..."); + const tx = await factory + .connect(sPairBootstrapper) + .createPair(oeth.address, addresses.mainnet.WETH, true); + + const receipt = await tx.wait(); + const pairCreatedEvent = receipt.events.find(e => e.topics[0] === pairCreatedTopic); + const [,pairAddress,] = ethers.utils.defaultAbiCoder.decode( + ["bool", "address", "uint256"], + pairCreatedEvent.data + ); + console.log("Pair address:", pairAddress); + + console.log("Whitelisting OETH token & WETH token as connector"); + await tokenHandler.connect(sTokenHandlerWhitelister).whitelistToken(oeth.address); + await tokenHandler.connect(sTokenHandlerWhitelister).whitelistToken(addresses.mainnet.WETH); + await tokenHandler.connect(sTokenHandlerWhitelister).whitelistConnector(addresses.mainnet.WETH); + + poolAddress = pairAddress; + + log("Creating gauge for OETH/WETH..."); + const gaugeCreatedTopic = ethers.utils.id("GaugeCreated(address,address,address,address,address)"); + const tx1 = await gaugeManager.createGauge(poolAddress, 0); + const receipt1 = await tx1.wait(); + const gaugeCreatedEvent = receipt1.events.find(e => e.topics[0] === gaugeCreatedTopic); + console.log("gaugeCreatedEvent", gaugeCreatedEvent) + //const gaugeAddress = gaugeCreatedEvent.topics[1]; + const gaugeAddress = ethers.utils.getAddress(`0x${gaugeCreatedEvent.topics[1].slice(-40)}`); + log(`Created gauge at ${gaugeAddress}`); + + return { poolAddress, gaugeAddress }; +}; + +const deployOETHSupernovaAMOStrategyImplementation = async (poolAddress, gaugeAddress) => { + const { deployerAddr } = await getNamedAccounts(); + const sDeployer = await ethers.provider.getSigner(deployerAddr); + + const cOETHSupernovaAMOStrategyProxy = await ethers.getContract( + "OETHSupernovaAMOProxy" + ); + const cOETHProxy = await ethers.getContract("OETHProxy"); + const cOETHVaultProxy = await ethers.getContract("OETHVaultProxy"); + + // Deploy Sonic SwapX AMO Strategy implementation that will serve + // OETH Supernova AMO + const dSupernovaAMOStrategy = await deployWithConfirmation( + "SupernovaAMOStrategy", + [ + [poolAddress, cOETHVaultProxy.address], + cOETHProxy.address, + addresses.mainnet.WETH, + gaugeAddress, + ], + "SonicSwapXAMOStrategy" + ); + + const cOETHSupernovaAMOStrategy = await ethers.getContractAt( + "SonicSwapXAMOStrategy", + cOETHSupernovaAMOStrategyProxy.address + ); + + // Initialize Sonic Curve AMO Strategy implementation + const depositPriceRange = parseUnits("0.01", 18); // 1% or 100 basis points + const initData = cOETHSupernovaAMOStrategy.interface.encodeFunctionData( + "initialize(address[],uint256)", + [[addresses.mainnet.supernovaToken], depositPriceRange] + ); + await withConfirmation( + // prettier-ignore + cOETHSupernovaAMOStrategyProxy + .connect(sDeployer)["initialize(address,address,bytes)"]( + dSupernovaAMOStrategy.address, + addresses.mainnet.Timelock, + initData + ) + ); + + return cOETHSupernovaAMOStrategy; +}; + const getCreate2ProxiesFilePath = async () => { const networkName = isFork || isForkTest || isCI ? "localhost" : await getNetworkName(); @@ -1999,6 +2110,8 @@ module.exports = { deployPlumeMockRoosterAMOStrategyImplementation, getPlumeContracts, deploySonicSwapXAMOStrategyImplementation, + deployOETHSupernovaAMOStrategyImplementation, + deployOETHSupernovaAMOStrategyPoolAndGauge, deployProxyWithCreateX, deployCrossChainMasterStrategyImpl, deployCrossChainRemoteStrategyImpl, diff --git a/contracts/deploy/mainnet/174_supernova_AMO.js b/contracts/deploy/mainnet/174_supernova_AMO.js new file mode 100644 index 0000000000..f74910b0ac --- /dev/null +++ b/contracts/deploy/mainnet/174_supernova_AMO.js @@ -0,0 +1,60 @@ +const addresses = require("../../utils/addresses"); +const { + deploymentWithGovernanceProposal, + deployWithConfirmation, +} = require("../../utils/deploy"); +const { + deployOETHSupernovaAMOStrategyImplementation, + deployOETHSupernovaAMOStrategyPoolAndGauge +} = require("../deployActions"); + +module.exports = deploymentWithGovernanceProposal( + { + deployName: "174_supernova_AMO", + }, + async ({ ethers }) => { + const cOETHVaultProxy = await ethers.getContract("OETHVaultProxy"); + const cOETHVaultAdmin = await ethers.getContractAt( + "VaultAdmin", + cOETHVaultProxy.address + ); + + // TODO: delete once the pools ang gauges are created + const { poolAddress, gaugeAddress } = await deployOETHSupernovaAMOStrategyPoolAndGauge(); + + await deployWithConfirmation("OETHSupernovaAMOProxy"); + const cOETHSupernovaAMOProxy = await ethers.getContract( + "OETHSupernovaAMOProxy" + ); + console.log("poolAddress", poolAddress); + console.log("gaugeAddress", gaugeAddress); + + // Deploy Sonic SwapX AMO Strategy implementation + const cSupernovaAMOStrategy = + await deployOETHSupernovaAMOStrategyImplementation(poolAddress, gaugeAddress); + + return { + name: "Deploy Supernova AMO Strategy", + actions: [ + // 1. Approve new strategy on the Vault + { + contract: cOETHVaultAdmin, + signature: "approveStrategy(address)", + args: [cOETHSupernovaAMOProxy.address], + }, + // 2. Add strategy to mint whitelist + { + contract: cOETHVaultAdmin, + signature: "addStrategyToMintWhitelist(address)", + args: [cOETHSupernovaAMOProxy.address], + }, + // 3. Set the Harvester on the Supernova AMO strategy + { + contract: cSupernovaAMOStrategy, + signature: "setHarvesterAddress(address)", + args: [addresses.multichainBuybackOperator], + }, + ], + }; + } +); diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 47c974481a..8f40e8e574 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -2567,33 +2567,11 @@ async function instantRebaseVaultFixture(tokenName) { return fixture; } -async function supernovaAMOFixure() { +async function supernovaOETHAMOFixure() { const fixture = await defaultFixture(); - const { ousd, usdc } = fixture; - - const factoryABI = [ - "function createPair(address tokenA, address tokenB) external returns (address pair)", - "function getPair(address tokenA, address tokenB) external view returns (address pair)" - ]; - const factory = await ethers.getContractAt(factoryABI, addresses.mainnet.supernovaPairFactory); - - const pairAddress = await factory.getPair(ousd.address, usdc.address); - let pair; - - if (pairAddress === ethers.constants.AddressZero) { - log("Creating new OUSD/USDC pair..."); - const tx = await factory.createPair(ousd.address, usdc.address); - const receipt = await tx.wait(); - const pairCreatedEvent = receipt.events.find(e => e.event === 'PairCreated'); - const newPairAddress = pairCreatedEvent.args.pair; - } else { - log(`Using existing OUSD/USDC pair: ${pairAddress}`); - } - - // TODO fetch strategy contract from deployment file - // use deploy actions + const { oeth, weth } = fixture; - fixture.supernovaPair = await ethers.getContractAt("IPair", pairAddress); + return fixture; } @@ -3119,5 +3097,5 @@ module.exports = { claimRewardsModuleFixture, crossChainFixtureUnit, crossChainFixture, - supernovaAMOFixure, + supernovaOETHAMOFixure, }; diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index 746a80010d..4ca806e416 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -396,6 +396,10 @@ addresses.mainnet.toConsensus.withdrawals = addresses.mainnet.supernovaPairFactory = "0x5aef44edfc5a7edd30826c724ea12d7be15bdc30"; +addresses.mainnet.supernovaGaugeManager = + "0x19a410046Afc4203AEcE5fbFc7A6Ac1a4F517AE2"; +addresses.mainnet.supernovaToken = + "0x00Da8466B296E382E5Da2Bf20962D0cB87200c78"; // Mainnet Merkl addresses.mainnet.CampaignCreator = From 70d2412f5a3e58f2f7d66bed980fecf36e511685 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 13 Feb 2026 00:03:55 +0100 Subject: [PATCH 03/44] commit for easier diffs --- .../IAlgebraGauge.sol} | 0 .../IAlgebraPair.sol} | 0 .../algebra/StableSwapAMMStrategy.sol | 834 ++++++++++++++++++ .../sonic/SonicSwapXAMOStrategy.sol | 800 +---------------- 4 files changed, 837 insertions(+), 797 deletions(-) rename contracts/contracts/interfaces/{sonic/ISwapXGauge.sol => algebra/IAlgebraGauge.sol} (100%) rename contracts/contracts/interfaces/{sonic/ISwapXPair.sol => algebra/IAlgebraPair.sol} (100%) create mode 100644 contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol diff --git a/contracts/contracts/interfaces/sonic/ISwapXGauge.sol b/contracts/contracts/interfaces/algebra/IAlgebraGauge.sol similarity index 100% rename from contracts/contracts/interfaces/sonic/ISwapXGauge.sol rename to contracts/contracts/interfaces/algebra/IAlgebraGauge.sol diff --git a/contracts/contracts/interfaces/sonic/ISwapXPair.sol b/contracts/contracts/interfaces/algebra/IAlgebraPair.sol similarity index 100% rename from contracts/contracts/interfaces/sonic/ISwapXPair.sol rename to contracts/contracts/interfaces/algebra/IAlgebraPair.sol diff --git a/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol b/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol new file mode 100644 index 0000000000..dcbd1df353 --- /dev/null +++ b/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol @@ -0,0 +1,834 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +/** + * @title Algebra Algorithmic Market Maker (AMO) Strategy + * @notice AMO strategy for the Algebra stable swap pool + * @author Origin Protocol Inc + */ +import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; + +import { IERC20, InitializableAbstractStrategy } from "../../utils/InitializableAbstractStrategy.sol"; +import { StableMath } from "../../utils/StableMath.sol"; +import { sqrt } from "../../utils/PRBMath.sol"; +import { IBasicToken } from "../../interfaces/IBasicToken.sol"; +import { IPair } from "../../interfaces/algebra/IAlgebraPair.sol"; +import { IGauge } from "../../interfaces/algebra/IAlgebraGauge.sol"; +import { IVault } from "../../interfaces/IVault.sol"; + +contract StableSwapAMMStrategy is InitializableAbstractStrategy { + using SafeERC20 for IERC20; + using StableMath for uint256; + using SafeCast for uint256; + + /** + * @notice a threshold under which the contract no longer allows for the protocol to manually rebalance. + * Guarding against a strategist / guardian being taken over and with multiple transactions + * draining the protocol funds. + */ + uint256 public constant SOLVENCY_THRESHOLD = 0.998 ether; + + /// @notice Precision for the Algebra Stable AMM (sAMM) invariant k. + uint256 public constant PRECISION = 1e18; + + /// @notice Address of the asset (non OToken) token contract + address public immutable asset; + + /// @notice Address of the OToken token contract. + address public immutable oToken; + + /// @notice Address of the Algebra Stable pool contract. + address public immutable pool; + + /// @notice Address of the Algebra Gauge contract. + address public immutable gauge; + + /// @notice The max amount the OToken/asset price can deviate from peg (1e18) + /// before deposits are reverted scaled to 18 decimals. + /// eg 0.01e18 or 1e16 is 1% which is 100 basis points. + /// This is the amount below and above peg so a 50 basis point deviation (0.005e18) + /// allows a price range from 0.995 to 1.005. + uint256 public maxDepeg; + + /// @notice Index of the OToken in the Algebra pool. + uint256 public oTokenPoolIndex; + + event SwapOTokensToPool( + uint256 oTokenMinted, + uint256 assetDepositAmount, + uint256 oTokenDepositAmount, + uint256 lpTokens + ); + event SwapAssetsToPool( + uint256 assetSwapped, + uint256 lpTokens, + uint256 oTokenBurnt + ); + event MaxDepegUpdated(uint256 maxDepeg); + + /** + * @dev Verifies that the caller is the Strategist of the Vault. + */ + modifier onlyStrategist() { + require( + msg.sender == IVault(vaultAddress).strategistAddr(), + "Caller is not the Strategist" + ); + _; + } + + /** + * @dev Skim the SwapX pool in case any extra wS or OS tokens were added + */ + modifier skimPool() { + IPair(pool).skim(address(this)); + _; + } + + /** + * @dev Checks the pool is balanced enough to allow deposits. + */ + modifier nearBalancedPool() { + // OToken/asset price = asset / OToken + // Get the OToken/asset price for selling 1 OToken for asset + // As OToken is 1, the asset amount is the OToken/asset price + uint256 sellPrice = IPair(pool).getAmountOut(1e18, oToken); + + // Get the amount of OToken received from selling 1 asset. This is buying OToken. + uint256 oTokenAmount = IPair(pool).getAmountOut(1e18, asset); + // Convert to a OToken/asset price = asset / OToken + uint256 buyPrice = 1e36 / oTokenAmount; + + uint256 pegPrice = 1e18; + + require( + sellPrice >= pegPrice - maxDepeg && buyPrice <= pegPrice + maxDepeg, + "price out of range" + ); + _; + } + + /** + * @dev Checks the pool's balances have improved and the balances + * have not tipped to the other side. + * This modifier is only applied to functions that do swaps against the pool. + * Deposits and withdrawals are proportional to the pool's balances hence don't need this check. + */ + modifier improvePoolBalance() { + // Get the asset and OToken balances in the pool + (uint256 assetReserveBefore, uint256 oTokenReserveBefore) = _getPoolReserves(); + // diff = wS balance - OS balance + int256 diffBefore = assetReserveBefore.toInt256() - + oTokenReserveBefore.toInt256(); + + _; + + // Get the asset and OToken balances in the pool + (uint256 assetReserveAfter, uint256 oTokenReserveAfter) = _getPoolReserves(); + // diff = asset balance - OToken balance + int256 diffAfter = assetReserveAfter.toInt256() - + oTokenReserveAfter.toInt256(); + + if (diffBefore == 0) { + require(diffAfter == 0, "Position balance is worsened"); + } else if (diffBefore < 0) { + // If the pool was originally imbalanced in favor of OToken, then + // we want to check that the pool is now more balanced + require(diffAfter <= 0, "Assets overshot peg"); + require(diffBefore < diffAfter, "OTokens balance worse"); + } else if (diffBefore > 0) { + // If the pool was originally imbalanced in favor of asset, then + // we want to check that the pool is now more balanced + require(diffAfter >= 0, "OTokens overshot peg"); + require(diffAfter < diffBefore, "Assets balance worse"); + } + } + + /** + * @param _baseConfig The `platformAddress` is the address of the SwapX pool. + * The `vaultAddress` is the address of the Origin Sonic Vault. + * @param _oToken Address of the OToken. + * @param _asset Address of the asset token. + * @param _gauge Address of the Algebra gauge for the pool. + */ + constructor( + BaseStrategyConfig memory _baseConfig, + address _oToken, + address _asset, + address _gauge + ) InitializableAbstractStrategy(_baseConfig) { + // Checked both tokens are to 18 decimals + require( + IBasicToken(_asset).decimals() == 18 && + IBasicToken(_oToken).decimals() == 18, + "Incorrect token decimals" + ); + // Check the SwapX pool is a Stable AMM (sAMM) + require( + IPair(_baseConfig.platformAddress).isStable() == true, + "Pool not stable" + ); + // Check the gauge is for the pool + require( + IGauge(_gauge).TOKEN() == _baseConfig.platformAddress, + "Incorrect gauge" + ); + oTokenPoolIndex = IPair(_baseConfig.platformAddress).token0() == _oToken ? 0 : 1; + // Check the pool tokens are correct + require( + IPair(_baseConfig.platformAddress).token0() == (oTokenPoolIndex == 0 ? _oToken : _asset) && + IPair(_baseConfig.platformAddress).token1() == (oTokenPoolIndex == 0 ? _asset : _oToken), + "Incorrect pool tokens" + ); + + // Set the immutable variables + oToken = _oToken; + asset = _asset; + pool = _baseConfig.platformAddress; + gauge = _gauge; + + // This is an implementation contract. The governor is set in the proxy contract. + _setGovernor(address(0)); + } + + /** + * Initializer for setting up strategy internal state. This overrides the + * InitializableAbstractStrategy initializer as SwapX strategies don't fit + * well within that abstraction. + * @param _rewardTokenAddresses Array containing SWPx token address + * @param _maxDepeg The max amount the OS/wS price can deviate from peg (1e18) before deposits are reverted. + */ + function initialize( + address[] calldata _rewardTokenAddresses, + uint256 _maxDepeg + ) external onlyGovernor initializer { + address[] memory pTokens = new address[](1); + pTokens[0] = pool; + + address[] memory _assets = new address[](1); + _assets[0] = asset; + + InitializableAbstractStrategy._initialize( + _rewardTokenAddresses, + _assets, + pTokens + ); + + maxDepeg = _maxDepeg; + + _approveBase(); + } + + /*************************************** + Deposit + ****************************************/ + + /** + * @notice Deposit an amount of asset into the Algebra pool. + * Mint OToken in proportion to the pool's asset and OToken reserves, + * transfer asset and OToken to the pool, + * mint the pool's LP token and deposit in the gauge. + * @dev This tx must be wrapped by the VaultValueChecker. + * To minimize loses, the pool should be rebalanced before depositing. + * The pool's oToken/asset price must be within the maxDepeg range. + * @param _asset Address of asset token. + * @param _assetAmount Amount of asset tokens to deposit. + */ + function deposit(address _asset, uint256 _assetAmount) + external + override + onlyVault + nonReentrant + skimPool + nearBalancedPool + { + require(_asset == asset, "Unsupported asset"); + require(_assetAmount > 0, "Must deposit something"); + + (uint256 oTokenDepositAmount, ) = _deposit(_assetAmount); + + // Ensure solvency of the vault + _solvencyAssert(); + + // Emit event for the deposited wS tokens + emit Deposit(asset, pool, _assetAmount); + // Emit event for the minted OToken tokens + emit Deposit(oToken, pool, oTokenDepositAmount); + } + + /** + * @notice Deposit all the strategy's asset tokens into the Algebra pool. + * Mint OToken in proportion to the pool's asset and OToken reserves, + * transfer asset and OToken to the pool, + * mint the pool's LP token and deposit in the gauge. + * @dev This tx must be wrapped by the VaultValueChecker. + * To minimize loses, the pool should be rebalanced before depositing. + * The pool's oToken/asset price must be within the maxDepeg range. + */ + function depositAll() + external + override + onlyVault + nonReentrant + skimPool + nearBalancedPool + { + uint256 assetBalance = IERC20(asset).balanceOf(address(this)); + if (assetBalance > 0) { + (uint256 oTokenDepositAmount, ) = _deposit(assetBalance); + + // Ensure solvency of the vault + _solvencyAssert(); + + // Emit event for the deposited asset tokens + emit Deposit(asset, pool, assetBalance); + // Emit event for the minted OToken tokens + emit Deposit(oToken, pool, oTokenDepositAmount); + } + } + + /** + * @dev Mint OToken in proportion to the pool's asset and OToken reserves, + * transfer asset and OToken to the pool, + * mint the pool's LP token and deposit in the gauge. + * @param _assetAmount Amount of asset tokens to deposit. + * @return oTokenDepositAmount Amount of OToken tokens minted and deposited into the pool. + * @return lpTokens Amount of SwapX pool LP tokens minted and deposited into the gauge. + */ + function _deposit(uint256 _assetAmount) + internal + returns (uint256 oTokenDepositAmount, uint256 lpTokens) + { + // Calculate the required amount of OToken to mint based on the asset amount. + oTokenDepositAmount = _calcTokensToMint(_assetAmount); + + // Mint the required OToken tokens to this strategy + IVault(vaultAddress).mintForStrategy(oTokenDepositAmount); + + // Add asset and OToken liquidity to the pool and stake in gauge + lpTokens = _depositToPoolAndGauge(_assetAmount, oTokenDepositAmount); + } + + /*************************************** + Withdraw + ****************************************/ + + /** + * @notice Withdraw wS and OS from the SwapX pool, burn the OS, + * and transfer the wS to the recipient. + * @param _recipient Address of the Vault. + * @param _asset Address of the Wrapped S (wS) contract. + * @param _wsAmount Amount of Wrapped S (wS) to withdraw. + */ + function withdraw( + address _recipient, + address _asset, + uint256 _wsAmount + ) external override onlyVault nonReentrant skimPool { + require(_wsAmount > 0, "Must withdraw something"); + require(_asset == asset, "Unsupported asset"); + // This strategy can't be set as a default strategy for wS in the Vault. + // This means the recipient must always be the Vault. + require(_recipient == vaultAddress, "Only withdraw to vault allowed"); + + // Calculate how much pool LP tokens to burn to get the required amount of wS tokens back + uint256 lpTokens = _calcTokensToBurn(_wsAmount); + + // Withdraw pool LP tokens from the gauge and remove assets from from the pool + _withdrawFromGaugeAndPool(lpTokens); + + // Burn all the removed OS and any that was left in the strategy + uint256 osToBurn = IERC20(oToken).balanceOf(address(this)); + IVault(vaultAddress).burnForStrategy(osToBurn); + + // Transfer wS to the recipient + // Note there can be a dust amount of wS left in the strategy as + // the burn of the pool's LP tokens is rounded up + require( + IERC20(asset).balanceOf(address(this)) >= _wsAmount, + "Not enough wS removed from pool" + ); + IERC20(asset).safeTransfer(_recipient, _wsAmount); + + // Ensure solvency of the vault + _solvencyAssert(); + + // Emit event for the withdrawn wS tokens + emit Withdrawal(asset, pool, _wsAmount); + // Emit event for the burnt OS tokens + emit Withdrawal(oToken, pool, osToBurn); + } + + /** + * @notice Withdraw all pool LP tokens from the gauge, + * remove all wS and OS from the SwapX pool, + * burn all the OS tokens, + * and transfer all the wS to the Vault contract. + * @dev There is no solvency check here as withdrawAll can be called to + * quickly secure assets to the Vault in emergencies. + */ + function withdrawAll() + external + override + onlyVaultOrGovernor + nonReentrant + skimPool + { + // Get all the pool LP tokens the strategy has staked in the gauge + uint256 lpTokens = IGauge(gauge).balanceOf(address(this)); + // Can not withdraw zero LP tokens from the gauge + if (lpTokens == 0) return; + + if (IGauge(gauge).emergency()) { + // The gauge is in emergency mode + _emergencyWithdrawFromGaugeAndPool(); + } else { + // Withdraw pool LP tokens from the gauge and remove assets from from the pool + _withdrawFromGaugeAndPool(lpTokens); + } + + // Burn all OS in this strategy contract + uint256 osToBurn = IERC20(oToken).balanceOf(address(this)); + IVault(vaultAddress).burnForStrategy(osToBurn); + + // Get the strategy contract's wS balance. + // This includes all that was removed from the SwapX pool and + // any that was sitting in the strategy contract before the removal. + uint256 wsBalance = IERC20(asset).balanceOf(address(this)); + IERC20(asset).safeTransfer(vaultAddress, wsBalance); + + // Emit event for the withdrawn wS tokens + emit Withdrawal(asset, pool, wsBalance); + // Emit event for the burnt OS tokens + emit Withdrawal(oToken, pool, osToBurn); + } + + /*************************************** + Pool Rebalancing + ****************************************/ + + /** @notice Used when there is more OS than wS in the pool. + * wS and OS is removed from the pool, the received wS is swapped for OS + * and the left over OS in the strategy is burnt. + * The OS/wS price is < 1.0 so OS is being bought at a discount. + * @param _wsAmount Amount of Wrapped S (wS) to swap into the pool. + */ + function swapAssetsToPool(uint256 _wsAmount) + external + onlyStrategist + nonReentrant + improvePoolBalance + skimPool + { + require(_wsAmount > 0, "Must swap something"); + + // 1. Partially remove liquidity so there’s enough wS for the swap + + // Calculate how much pool LP tokens to burn to get the required amount of wS tokens back + uint256 lpTokens = _calcTokensToBurn(_wsAmount); + require(lpTokens > 0, "No LP tokens to burn"); + + _withdrawFromGaugeAndPool(lpTokens); + + // 2. Swap wS for OS against the pool + // Swap exact amount of wS for OS against the pool + // There can be a dust amount of wS left in the strategy as the burn of the pool's LP tokens is rounded up + _swapExactTokensForTokens(_wsAmount, asset, oToken); + + // 3. Burn all the OS left in the strategy from the remove liquidity and swap + uint256 osToBurn = IERC20(oToken).balanceOf(address(this)); + IVault(vaultAddress).burnForStrategy(osToBurn); + + // Ensure solvency of the vault + _solvencyAssert(); + + // Emit event for the burnt OS tokens + emit Withdrawal(oToken, pool, osToBurn); + // Emit event for the swap + emit SwapAssetsToPool(_wsAmount, lpTokens, osToBurn); + } + + /** + * @notice Used when there is more wS than OS in the pool. + * OS is minted and swapped for wS against the pool, + * more OS is minted and added back into the pool with the swapped out wS. + * The OS/wS price is > 1.0 so OS is being sold at a premium. + * @param _osAmount Amount of OS to swap into the pool. + */ + function swapOTokensToPool(uint256 _osAmount) + external + onlyStrategist + nonReentrant + improvePoolBalance + skimPool + { + require(_osAmount > 0, "Must swap something"); + + // 1. Mint OS so it can be swapped into the pool + + // There can be OS in the strategy from skimming the pool + uint256 osInStrategy = IERC20(oToken).balanceOf(address(this)); + require(_osAmount >= osInStrategy, "Too much OS in strategy"); + uint256 osToMint = _osAmount - osInStrategy; + + // Mint the required OS tokens to this strategy + IVault(vaultAddress).mintForStrategy(osToMint); + + // 2. Swap OS for wS against the pool + _swapExactTokensForTokens(_osAmount, oToken, asset); + + // The wS is from the swap and any wS that was sitting in the strategy + uint256 wsDepositAmount = IERC20(asset).balanceOf(address(this)); + + // 3. Add wS and OS back to the pool in proportion to the pool's reserves + (uint256 osDepositAmount, uint256 lpTokens) = _deposit(wsDepositAmount); + + // Ensure solvency of the vault + _solvencyAssert(); + + // Emit event for the minted OS tokens + emit Deposit(oToken, pool, osToMint + osDepositAmount); + // Emit event for the swap + emit SwapOTokensToPool( + osToMint, + wsDepositAmount, + osDepositAmount, + lpTokens + ); + } + + /*************************************** + Assets and Rewards + ****************************************/ + + /** + * @notice Get the wS value of assets in the strategy and SwapX pool. + * The value of the assets in the pool is calculated assuming the pool is balanced. + * This way the value can not be manipulated by changing the pool's token balances. + * @param _asset Address of the Wrapped S (wS) token + * @return balance Total value in wS. + */ + function checkBalance(address _asset) + external + view + override + returns (uint256 balance) + { + require(_asset == asset, "Unsupported asset"); + + // wS balance needed here for the balance check that happens from vault during depositing. + balance = IERC20(asset).balanceOf(address(this)); + + // This assumes 1 gauge LP token = 1 pool LP token + uint256 lpTokens = IGauge(gauge).balanceOf(address(this)); + if (lpTokens == 0) return balance; + + // Add the strategy’s share of the wS and OS tokens in the SwapX pool if the pool was balanced. + balance += _lpValue(lpTokens); + } + + /** + * @notice Returns bool indicating whether asset is supported by strategy + * @param _asset Address of the asset + */ + function supportsAsset(address _asset) public view override returns (bool) { + return _asset == asset; + } + + /** + * @notice Collect accumulated SWPx (and other) rewards and send to the Harvester. + */ + function collectRewardTokens() + external + override + onlyHarvester + nonReentrant + { + // Collect SWPx rewards from the gauge + IGauge(gauge).getReward(); + + _collectRewardTokens(); + } + + /*************************************** + Internal SwapX Pool and Gauge Functions + ****************************************/ + + /** + * @dev Calculate the required amount of OS to mint based on the wS amount. + * This ensures the proportion of OS tokens being added to the pool matches the proportion of wS tokens. + * For example, if the added wS tokens is 10% of existing wS tokens in the pool, + * then the OS tokens being added should also be 10% of the OS tokens in the pool. + * @param _wsAmount Amount of Wrapped S (wS) to be added to the pool. + * @return osAmount Amount of OS to be minted and added to the pool. + */ + function _calcTokensToMint(uint256 _wsAmount) + internal + view + returns (uint256 osAmount) + { + (uint256 wsReserves, uint256 osReserves, ) = IPair(pool).getReserves(); + require(wsReserves > 0, "Empty pool"); + + // OS to add = (wS being added * OS in pool) / wS in pool + osAmount = (_wsAmount * osReserves) / wsReserves; + } + + /** + * @dev Calculate how much pool LP tokens to burn to get the required amount of wS tokens back + * from the pool. + * @param _wsAmount Amount of Wrapped S (wS) to be removed from the pool. + * @return lpTokens Amount of SwapX pool LP tokens to burn. + */ + function _calcTokensToBurn(uint256 _wsAmount) + internal + view + returns (uint256 lpTokens) + { + /* The SwapX pool proportionally returns the reserve tokens when removing liquidity. + * First, calculate the proportion of required wS tokens against the pools wS reserves. + * That same proportion is used to calculate the required amount of pool LP tokens. + * For example, if the required wS tokens is 10% of the pool's wS reserves, + * then 10% of the pool's LP supply needs to be burned. + * + * Because we are doing balanced removal we should be making profit when removing liquidity in a + * pool tilted to either side. + * + * Important: A downside is that the Strategist / Governor needs to be + * cognizant of not removing too much liquidity. And while the proposal to remove liquidity + * is being voted on, the pool tilt might change so much that the proposal that has been valid while + * created is no longer valid. + */ + + (uint256 wsReserves, , ) = IPair(pool).getReserves(); + require(wsReserves > 0, "Empty pool"); + + lpTokens = (_wsAmount * IPair(pool).totalSupply()) / wsReserves; + lpTokens += 1; // Add 1 to ensure we get enough LP tokens with rounding + } + + /** + * @dev Deposit Wrapped S (wS) and OS liquidity to the SwapX pool + * and stake the pool's LP token in the gauge. + * @param _wsAmount Amount of Wrapped S (wS) to deposit. + * @param _osAmount Amount of OS to deposit. + * @return lpTokens Amount of SwapX pool LP tokens minted. + */ + function _depositToPoolAndGauge(uint256 _wsAmount, uint256 _osAmount) + internal + returns (uint256 lpTokens) + { + // Transfer wS to the pool + IERC20(asset).safeTransfer(pool, _wsAmount); + // Transfer OS to the pool + IERC20(oToken).safeTransfer(pool, _osAmount); + + // Mint LP tokens from the pool + lpTokens = IPair(pool).mint(address(this)); + + // Deposit the pool's LP tokens into the gauge + IGauge(gauge).deposit(lpTokens); + } + + /** + * @dev Withdraw pool LP tokens from the gauge and remove wS and OS from the pool. + * @param _lpTokens Amount of SwapX pool LP tokens to withdraw from the gauge + */ + function _withdrawFromGaugeAndPool(uint256 _lpTokens) internal { + require( + IGauge(gauge).balanceOf(address(this)) >= _lpTokens, + "Not enough LP tokens in gauge" + ); + + // Withdraw pool LP tokens from the gauge + IGauge(gauge).withdraw(_lpTokens); + + // Transfer the pool LP tokens to the pool + IERC20(pool).safeTransfer(pool, _lpTokens); + + // Burn the LP tokens and transfer the wS and OS back to the strategy + IPair(pool).burn(address(this)); + } + + /** + * @dev Withdraw all pool LP tokens from the gauge when it's in emergency mode + * and remove wS and OS from the pool. + */ + function _emergencyWithdrawFromGaugeAndPool() internal { + // Withdraw all pool LP tokens from the gauge + IGauge(gauge).emergencyWithdraw(); + + // Get the pool LP tokens in strategy + uint256 _lpTokens = IERC20(pool).balanceOf(address(this)); + + // Transfer the pool LP tokens to the pool + IERC20(pool).safeTransfer(pool, _lpTokens); + + // Burn the LP tokens and transfer the wS and OS back to the strategy + IPair(pool).burn(address(this)); + } + + /** + * @dev Swap exact amount of tokens for another token against the pool. + * @param _amountIn Amount of tokens to swap into the pool. + * @param _tokenIn Address of the token going into the pool. + * @param _tokenOut Address of the token being swapped out of the pool. + */ + function _swapExactTokensForTokens( + uint256 _amountIn, + address _tokenIn, + address _tokenOut + ) internal { + // Transfer in tokens to the pool + IERC20(_tokenIn).safeTransfer(pool, _amountIn); + + // Calculate how much out tokens we get from the swap + uint256 amountOut = IPair(pool).getAmountOut(_amountIn, _tokenIn); + + // Safety check that we are dealing with the correct pool tokens + require( + (_tokenIn == asset && _tokenOut == oToken) || + (_tokenIn == oToken && _tokenOut == asset), + "Unsupported swap" + ); + + // Work out the correct order of the amounts for the pool + (uint256 amount0, uint256 amount1) = _tokenIn == asset + ? (uint256(0), amountOut) + : (amountOut, 0); + + // Perform the swap on the pool + IPair(pool).swap(amount0, amount1, address(this), new bytes(0)); + + // The slippage protection against the amount out is indirectly done + // via the improvePoolBalance + } + + /// @dev Calculate the value of a LP position in a SwapX stable pool + /// if the pool was balanced. + /// @param _lpTokens Amount of LP tokens in the SwapX pool + /// @return value The wS value of the LP tokens when the pool is balanced + function _lpValue(uint256 _lpTokens) internal view returns (uint256 value) { + // Get total supply of LP tokens + uint256 totalSupply = IPair(pool).totalSupply(); + if (totalSupply == 0) return 0; + + // Get the current reserves of the pool + (uint256 wsReserves, uint256 osReserves, ) = IPair(pool).getReserves(); + + // Calculate the invariant of the pool assuming both tokens have 18 decimals. + // k is scaled to 18 decimals. + uint256 k = _invariant(wsReserves, osReserves); + + // If x = y, let’s denote x = y = z (where z is the common reserve value) + // Substitute z into the invariant: + // k = z^3 * z + z * z^3 + // k = 2 * z^4 + // Going back the other way to calculate the common reserve value z + // z = (k / 2) ^ (1/4) + // the total value of the pool when x = y is 2 * z, which is 2 * (k / 2) ^ (1/4) + uint256 zSquared = sqrt((k * 1e18) / 2); // 18 + 18 = 36 decimals becomes 18 decimals after sqrt + uint256 z = sqrt(zSquared * 1e18); // 18 + 18 = 36 decimals becomes 18 decimals after sqrt + uint256 totalValueOfPool = 2 * z; + + // lp value = lp tokens * value of pool / total supply + value = (_lpTokens * totalValueOfPool) / totalSupply; + } + + /** + * @dev Compute the invariant for a SwapX stable pool. + * This assumed both x and y tokens are to 18 decimals which is checked in the constructor. + * invariant: k = x^3 * y + x * y^3 + * @dev This implementation is copied from SwapX's Pair contract. + * @param _x The amount of Wrapped S (wS) tokens in the pool + * @param _y The amount of the OS tokens in the pool + * @return k The invariant of the SwapX stable pool + */ + function _invariant(uint256 _x, uint256 _y) + internal + pure + returns (uint256 k) + { + uint256 _a = (_x * _y) / PRECISION; + uint256 _b = ((_x * _x) / PRECISION + (_y * _y) / PRECISION); + // slither-disable-next-line divide-before-multiply + k = (_a * _b) / PRECISION; + } + + /** + * @dev Checks that the protocol is solvent, protecting from a rogue Strategist / Guardian that can + * keep rebalancing the pool in both directions making the protocol lose a tiny amount of + * funds each time. + * + * Protocol must be at least SOLVENCY_THRESHOLD (99,8 %) backed in order for the rebalances to + * function. + */ + function _solvencyAssert() internal view { + uint256 _totalVaultValue = IVault(vaultAddress).totalValue(); + uint256 _totalSupply = IERC20(oToken).totalSupply(); + + if ( + _totalSupply > 0 && + _totalVaultValue.divPrecisely(_totalSupply) < SOLVENCY_THRESHOLD + ) { + revert("Protocol insolvent"); + } + } + + /** + * @dev Get the reserves of the pool no matter the order of tokens in the underlying + * Algebra pool. + * @return assetReserves The reserves of the asset token in the pool. + * @return oTokenReserves The reserves of the OToken token in the pool. + */ + function _getPoolReserves() internal view returns (uint256 assetReserves, uint256 oTokenReserves) { + (uint256 reserve0, uint256 reserve1, ) = IPair(pool).getReserves(); + assetReserves = oTokenPoolIndex == 0 ? reserve1 : reserve0; + oTokenReserves = oTokenPoolIndex == 0 ? reserve0 : reserve1; + } + /*************************************** + Setters + ****************************************/ + + /** + * @notice Set the maximum deviation from the OS/wS peg (1e18) before deposits are reverted. + * @param _maxDepeg the OS/wS price from peg (1e18) in 18 decimals. + * eg 0.01e18 or 1e16 is 1% which is 100 basis points. + */ + function setMaxDepeg(uint256 _maxDepeg) external onlyGovernor { + maxDepeg = _maxDepeg; + + emit MaxDepegUpdated(_maxDepeg); + } + + /*************************************** + Approvals + ****************************************/ + + /** + * @notice Approve the spending of all assets by their corresponding pool tokens, + * if for some reason is it necessary. + */ + function safeApproveAllTokens() + external + override + onlyGovernor + nonReentrant + { + _approveBase(); + } + + // solhint-disable-next-line no-unused-vars + function _abstractSetPToken(address _asset, address _pToken) + internal + override + {} + + function _approveBase() internal { + // Approve SwapX gauge contract to transfer SwapX pool LP tokens + // This is needed for deposits of SwapX pool LP tokens into the gauge. + // slither-disable-next-line unused-return + IPair(pool).approve(address(gauge), type(uint256).max); + } +} diff --git a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol index 1f1206a032..b0bfe787a4 100644 --- a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol +++ b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol @@ -6,143 +6,9 @@ pragma solidity ^0.8.0; * @notice AMO strategy for the SwapX OS/wS stable pool * @author Origin Protocol Inc */ -import { SafeCast } from "@openzeppelin/contracts/utils/math/SafeCast.sol"; -import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { StableSwapAMMStrategy } from "../algebra/StableSwapAMMStrategy.sol"; -import { IERC20, InitializableAbstractStrategy } from "../../utils/InitializableAbstractStrategy.sol"; -import { StableMath } from "../../utils/StableMath.sol"; -import { sqrt } from "../../utils/PRBMath.sol"; -import { IBasicToken } from "../../interfaces/IBasicToken.sol"; -import { IPair } from "../../interfaces/sonic/ISwapXPair.sol"; -import { IGauge } from "../../interfaces/sonic/ISwapXGauge.sol"; -import { IVault } from "../../interfaces/IVault.sol"; - -contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { - using SafeERC20 for IERC20; - using StableMath for uint256; - using SafeCast for uint256; - - /** - * @notice a threshold under which the contract no longer allows for the protocol to manually rebalance. - * Guarding against a strategist / guardian being taken over and with multiple transactions - * draining the protocol funds. - */ - uint256 public constant SOLVENCY_THRESHOLD = 0.998 ether; - - /// @notice Precision for the SwapX Stable AMM (sAMM) invariant k. - uint256 public constant PRECISION = 1e18; - - /// @notice Address of the Wrapped S (wS) token. - address public immutable ws; - - /// @notice Address of the OS token contract. - address public immutable os; - - /// @notice Address of the SwapX Stable pool contract. - address public immutable pool; - - /// @notice Address of the SwapX Gauge contract. - address public immutable gauge; - - /// @notice The max amount the OS/wS price can deviate from peg (1e18) - /// before deposits are reverted scaled to 18 decimals. - /// eg 0.01e18 or 1e16 is 1% which is 100 basis points. - /// This is the amount below and above peg so a 50 basis point deviation (0.005e18) - /// allows a price range from 0.995 to 1.005. - uint256 public maxDepeg; - - event SwapOTokensToPool( - uint256 osMinted, - uint256 wsDepositAmount, - uint256 osDepositAmount, - uint256 lpTokens - ); - event SwapAssetsToPool( - uint256 wsSwapped, - uint256 lpTokens, - uint256 osBurnt - ); - event MaxDepegUpdated(uint256 maxDepeg); - - /** - * @dev Verifies that the caller is the Strategist of the Vault. - */ - modifier onlyStrategist() { - require( - msg.sender == IVault(vaultAddress).strategistAddr(), - "Caller is not the Strategist" - ); - _; - } - - /** - * @dev Skim the SwapX pool in case any extra wS or OS tokens were added - */ - modifier skimPool() { - IPair(pool).skim(address(this)); - _; - } - - /** - * @dev Checks the pool is balanced enough to allow deposits. - */ - modifier nearBalancedPool() { - // OS/wS price = wS / OS - // Get the OS/wS price for selling 1 OS for wS - // As OS is 1, the wS amount is the OS/wS price - uint256 sellPrice = IPair(pool).getAmountOut(1e18, os); - - // Get the amount of OS received from selling 1 wS. This is buying OS. - uint256 osAmount = IPair(pool).getAmountOut(1e18, ws); - // Convert to a OS/wS price = wS / OS - uint256 buyPrice = 1e36 / osAmount; - - uint256 pegPrice = 1e18; - - require( - sellPrice >= pegPrice - maxDepeg && buyPrice <= pegPrice + maxDepeg, - "price out of range" - ); - _; - } - - /** - * @dev Checks the pool's balances have improved and the balances - * have not tipped to the other side. - * This modifier is only applied to functions that do swaps against the pool. - * Deposits and withdrawals are proportional to the pool's balances hence don't need this check. - */ - modifier improvePoolBalance() { - // Get the asset and OToken balances in the pool - (uint256 wsReservesBefore, uint256 osReservesBefore, ) = IPair(pool) - .getReserves(); - // diff = wS balance - OS balance - int256 diffBefore = wsReservesBefore.toInt256() - - osReservesBefore.toInt256(); - - _; - - // Get the asset and OToken balances in the pool - (uint256 wsReservesAfter, uint256 osReservesAfter, ) = IPair(pool) - .getReserves(); - // diff = wS balance - OS balance - int256 diffAfter = wsReservesAfter.toInt256() - - osReservesAfter.toInt256(); - - if (diffBefore == 0) { - require(diffAfter == 0, "Position balance is worsened"); - } else if (diffBefore < 0) { - // If the pool was originally imbalanced in favor of OS, then - // we want to check that the pool is now more balanced - require(diffAfter <= 0, "Assets overshot peg"); - require(diffBefore < diffAfter, "OTokens balance worse"); - } else if (diffBefore > 0) { - // If the pool was originally imbalanced in favor of wS, then - // we want to check that the pool is now more balanced - require(diffAfter >= 0, "OTokens overshot peg"); - require(diffAfter < diffBefore, "Assets balance worse"); - } - } +contract SonicSwapXAMOStrategy is StableSwapAMMStrategy { /** * @param _baseConfig The `platformAddress` is the address of the SwapX pool. @@ -156,666 +22,6 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy { address _os, address _ws, address _gauge - ) InitializableAbstractStrategy(_baseConfig) { - // Check the pool tokens are correct - require( - IPair(_baseConfig.platformAddress).token0() == _ws && - IPair(_baseConfig.platformAddress).token1() == _os, - "Incorrect pool tokens" - ); - // Checked both tokens are to 18 decimals - require( - IBasicToken(_ws).decimals() == 18 && - IBasicToken(_os).decimals() == 18, - "Incorrect token decimals" - ); - // Check the SwapX pool is a Stable AMM (sAMM) - require( - IPair(_baseConfig.platformAddress).isStable() == true, - "Pool not stable" - ); - // Check the gauge is for the pool - require( - IGauge(_gauge).TOKEN() == _baseConfig.platformAddress, - "Incorrect gauge" - ); - - // Set the immutable variables - os = _os; - ws = _ws; - pool = _baseConfig.platformAddress; - gauge = _gauge; - - // This is an implementation contract. The governor is set in the proxy contract. - _setGovernor(address(0)); - } - - /** - * Initializer for setting up strategy internal state. This overrides the - * InitializableAbstractStrategy initializer as SwapX strategies don't fit - * well within that abstraction. - * @param _rewardTokenAddresses Array containing SWPx token address - * @param _maxDepeg The max amount the OS/wS price can deviate from peg (1e18) before deposits are reverted. - */ - function initialize( - address[] calldata _rewardTokenAddresses, - uint256 _maxDepeg - ) external onlyGovernor initializer { - address[] memory pTokens = new address[](1); - pTokens[0] = pool; - - address[] memory _assets = new address[](1); - _assets[0] = ws; - - InitializableAbstractStrategy._initialize( - _rewardTokenAddresses, - _assets, - pTokens - ); - - maxDepeg = _maxDepeg; - - _approveBase(); - } - - /*************************************** - Deposit - ****************************************/ - - /** - * @notice Deposit an amount of Wrapped S (wS) into the SwapX pool. - * Mint OS in proportion to the pool's wS and OS reserves, - * transfer Wrapped S (wS) and OS to the pool, - * mint the pool's LP token and deposit in the gauge. - * @dev This tx must be wrapped by the VaultValueChecker. - * To minimize loses, the pool should be rebalanced before depositing. - * The pool's OS/wS price must be within the maxDepeg range. - * @param _asset Address of Wrapped S (wS) token. - * @param _wsAmount Amount of Wrapped S (wS) tokens to deposit. - */ - function deposit(address _asset, uint256 _wsAmount) - external - override - onlyVault - nonReentrant - skimPool - nearBalancedPool - { - require(_asset == ws, "Unsupported asset"); - require(_wsAmount > 0, "Must deposit something"); - - (uint256 osDepositAmount, ) = _deposit(_wsAmount); - - // Ensure solvency of the vault - _solvencyAssert(); - - // Emit event for the deposited wS tokens - emit Deposit(ws, pool, _wsAmount); - // Emit event for the minted OS tokens - emit Deposit(os, pool, osDepositAmount); - } - - /** - * @notice Deposit all the strategy's Wrapped S (wS) tokens into the SwapX pool. - * Mint OS in proportion to the pool's wS and OS reserves, - * transfer Wrapped S (wS) and OS to the pool, - * mint the pool's LP token and deposit in the gauge. - * @dev This tx must be wrapped by the VaultValueChecker. - * To minimize loses, the pool should be rebalanced before depositing. - * The pool's OS/wS price must be within the maxDepeg range. - */ - function depositAll() - external - override - onlyVault - nonReentrant - skimPool - nearBalancedPool - { - uint256 wsBalance = IERC20(ws).balanceOf(address(this)); - if (wsBalance > 0) { - (uint256 osDepositAmount, ) = _deposit(wsBalance); - - // Ensure solvency of the vault - _solvencyAssert(); - - // Emit event for the deposited wS tokens - emit Deposit(ws, pool, wsBalance); - // Emit event for the minted OS tokens - emit Deposit(os, pool, osDepositAmount); - } - } - - /** - * @dev Mint OS in proportion to the pool's wS and OS reserves, - * transfer Wrapped S (wS) and OS to the pool, - * mint the pool's LP token and deposit in the gauge. - * @param _wsAmount Amount of Wrapped S (wS) tokens to deposit. - * @return osDepositAmount Amount of OS tokens minted and deposited into the pool. - * @return lpTokens Amount of SwapX pool LP tokens minted and deposited into the gauge. - */ - function _deposit(uint256 _wsAmount) - internal - returns (uint256 osDepositAmount, uint256 lpTokens) - { - // Calculate the required amount of OS to mint based on the wS amount. - osDepositAmount = _calcTokensToMint(_wsAmount); - - // Mint the required OS tokens to this strategy - IVault(vaultAddress).mintForStrategy(osDepositAmount); - - // Add wS and OS liquidity to the pool and stake in gauge - lpTokens = _depositToPoolAndGauge(_wsAmount, osDepositAmount); - } - - /*************************************** - Withdraw - ****************************************/ - - /** - * @notice Withdraw wS and OS from the SwapX pool, burn the OS, - * and transfer the wS to the recipient. - * @param _recipient Address of the Vault. - * @param _asset Address of the Wrapped S (wS) contract. - * @param _wsAmount Amount of Wrapped S (wS) to withdraw. - */ - function withdraw( - address _recipient, - address _asset, - uint256 _wsAmount - ) external override onlyVault nonReentrant skimPool { - require(_wsAmount > 0, "Must withdraw something"); - require(_asset == ws, "Unsupported asset"); - // This strategy can't be set as a default strategy for wS in the Vault. - // This means the recipient must always be the Vault. - require(_recipient == vaultAddress, "Only withdraw to vault allowed"); - - // Calculate how much pool LP tokens to burn to get the required amount of wS tokens back - uint256 lpTokens = _calcTokensToBurn(_wsAmount); - - // Withdraw pool LP tokens from the gauge and remove assets from from the pool - _withdrawFromGaugeAndPool(lpTokens); - - // Burn all the removed OS and any that was left in the strategy - uint256 osToBurn = IERC20(os).balanceOf(address(this)); - IVault(vaultAddress).burnForStrategy(osToBurn); - - // Transfer wS to the recipient - // Note there can be a dust amount of wS left in the strategy as - // the burn of the pool's LP tokens is rounded up - require( - IERC20(ws).balanceOf(address(this)) >= _wsAmount, - "Not enough wS removed from pool" - ); - IERC20(ws).safeTransfer(_recipient, _wsAmount); - - // Ensure solvency of the vault - _solvencyAssert(); - - // Emit event for the withdrawn wS tokens - emit Withdrawal(ws, pool, _wsAmount); - // Emit event for the burnt OS tokens - emit Withdrawal(os, pool, osToBurn); - } - - /** - * @notice Withdraw all pool LP tokens from the gauge, - * remove all wS and OS from the SwapX pool, - * burn all the OS tokens, - * and transfer all the wS to the Vault contract. - * @dev There is no solvency check here as withdrawAll can be called to - * quickly secure assets to the Vault in emergencies. - */ - function withdrawAll() - external - override - onlyVaultOrGovernor - nonReentrant - skimPool - { - // Get all the pool LP tokens the strategy has staked in the gauge - uint256 lpTokens = IGauge(gauge).balanceOf(address(this)); - // Can not withdraw zero LP tokens from the gauge - if (lpTokens == 0) return; - - if (IGauge(gauge).emergency()) { - // The gauge is in emergency mode - _emergencyWithdrawFromGaugeAndPool(); - } else { - // Withdraw pool LP tokens from the gauge and remove assets from from the pool - _withdrawFromGaugeAndPool(lpTokens); - } - - // Burn all OS in this strategy contract - uint256 osToBurn = IERC20(os).balanceOf(address(this)); - IVault(vaultAddress).burnForStrategy(osToBurn); - - // Get the strategy contract's wS balance. - // This includes all that was removed from the SwapX pool and - // any that was sitting in the strategy contract before the removal. - uint256 wsBalance = IERC20(ws).balanceOf(address(this)); - IERC20(ws).safeTransfer(vaultAddress, wsBalance); - - // Emit event for the withdrawn wS tokens - emit Withdrawal(ws, pool, wsBalance); - // Emit event for the burnt OS tokens - emit Withdrawal(os, pool, osToBurn); - } - - /*************************************** - Pool Rebalancing - ****************************************/ - - /** @notice Used when there is more OS than wS in the pool. - * wS and OS is removed from the pool, the received wS is swapped for OS - * and the left over OS in the strategy is burnt. - * The OS/wS price is < 1.0 so OS is being bought at a discount. - * @param _wsAmount Amount of Wrapped S (wS) to swap into the pool. - */ - function swapAssetsToPool(uint256 _wsAmount) - external - onlyStrategist - nonReentrant - improvePoolBalance - skimPool - { - require(_wsAmount > 0, "Must swap something"); - - // 1. Partially remove liquidity so there’s enough wS for the swap - - // Calculate how much pool LP tokens to burn to get the required amount of wS tokens back - uint256 lpTokens = _calcTokensToBurn(_wsAmount); - require(lpTokens > 0, "No LP tokens to burn"); - - _withdrawFromGaugeAndPool(lpTokens); - - // 2. Swap wS for OS against the pool - // Swap exact amount of wS for OS against the pool - // There can be a dust amount of wS left in the strategy as the burn of the pool's LP tokens is rounded up - _swapExactTokensForTokens(_wsAmount, ws, os); - - // 3. Burn all the OS left in the strategy from the remove liquidity and swap - uint256 osToBurn = IERC20(os).balanceOf(address(this)); - IVault(vaultAddress).burnForStrategy(osToBurn); - - // Ensure solvency of the vault - _solvencyAssert(); - - // Emit event for the burnt OS tokens - emit Withdrawal(os, pool, osToBurn); - // Emit event for the swap - emit SwapAssetsToPool(_wsAmount, lpTokens, osToBurn); - } - - /** - * @notice Used when there is more wS than OS in the pool. - * OS is minted and swapped for wS against the pool, - * more OS is minted and added back into the pool with the swapped out wS. - * The OS/wS price is > 1.0 so OS is being sold at a premium. - * @param _osAmount Amount of OS to swap into the pool. - */ - function swapOTokensToPool(uint256 _osAmount) - external - onlyStrategist - nonReentrant - improvePoolBalance - skimPool - { - require(_osAmount > 0, "Must swap something"); - - // 1. Mint OS so it can be swapped into the pool - - // There can be OS in the strategy from skimming the pool - uint256 osInStrategy = IERC20(os).balanceOf(address(this)); - require(_osAmount >= osInStrategy, "Too much OS in strategy"); - uint256 osToMint = _osAmount - osInStrategy; - - // Mint the required OS tokens to this strategy - IVault(vaultAddress).mintForStrategy(osToMint); - - // 2. Swap OS for wS against the pool - _swapExactTokensForTokens(_osAmount, os, ws); - - // The wS is from the swap and any wS that was sitting in the strategy - uint256 wsDepositAmount = IERC20(ws).balanceOf(address(this)); - - // 3. Add wS and OS back to the pool in proportion to the pool's reserves - (uint256 osDepositAmount, uint256 lpTokens) = _deposit(wsDepositAmount); - - // Ensure solvency of the vault - _solvencyAssert(); - - // Emit event for the minted OS tokens - emit Deposit(os, pool, osToMint + osDepositAmount); - // Emit event for the swap - emit SwapOTokensToPool( - osToMint, - wsDepositAmount, - osDepositAmount, - lpTokens - ); - } - - /*************************************** - Assets and Rewards - ****************************************/ - - /** - * @notice Get the wS value of assets in the strategy and SwapX pool. - * The value of the assets in the pool is calculated assuming the pool is balanced. - * This way the value can not be manipulated by changing the pool's token balances. - * @param _asset Address of the Wrapped S (wS) token - * @return balance Total value in wS. - */ - function checkBalance(address _asset) - external - view - override - returns (uint256 balance) - { - require(_asset == ws, "Unsupported asset"); - - // wS balance needed here for the balance check that happens from vault during depositing. - balance = IERC20(ws).balanceOf(address(this)); - - // This assumes 1 gauge LP token = 1 pool LP token - uint256 lpTokens = IGauge(gauge).balanceOf(address(this)); - if (lpTokens == 0) return balance; - - // Add the strategy’s share of the wS and OS tokens in the SwapX pool if the pool was balanced. - balance += _lpValue(lpTokens); - } - - /** - * @notice Returns bool indicating whether asset is supported by strategy - * @param _asset Address of the asset - */ - function supportsAsset(address _asset) public view override returns (bool) { - return _asset == ws; - } - - /** - * @notice Collect accumulated SWPx (and other) rewards and send to the Harvester. - */ - function collectRewardTokens() - external - override - onlyHarvester - nonReentrant - { - // Collect SWPx rewards from the gauge - IGauge(gauge).getReward(); - - _collectRewardTokens(); - } - - /*************************************** - Internal SwapX Pool and Gauge Functions - ****************************************/ - - /** - * @dev Calculate the required amount of OS to mint based on the wS amount. - * This ensures the proportion of OS tokens being added to the pool matches the proportion of wS tokens. - * For example, if the added wS tokens is 10% of existing wS tokens in the pool, - * then the OS tokens being added should also be 10% of the OS tokens in the pool. - * @param _wsAmount Amount of Wrapped S (wS) to be added to the pool. - * @return osAmount Amount of OS to be minted and added to the pool. - */ - function _calcTokensToMint(uint256 _wsAmount) - internal - view - returns (uint256 osAmount) - { - (uint256 wsReserves, uint256 osReserves, ) = IPair(pool).getReserves(); - require(wsReserves > 0, "Empty pool"); - - // OS to add = (wS being added * OS in pool) / wS in pool - osAmount = (_wsAmount * osReserves) / wsReserves; - } - - /** - * @dev Calculate how much pool LP tokens to burn to get the required amount of wS tokens back - * from the pool. - * @param _wsAmount Amount of Wrapped S (wS) to be removed from the pool. - * @return lpTokens Amount of SwapX pool LP tokens to burn. - */ - function _calcTokensToBurn(uint256 _wsAmount) - internal - view - returns (uint256 lpTokens) - { - /* The SwapX pool proportionally returns the reserve tokens when removing liquidity. - * First, calculate the proportion of required wS tokens against the pools wS reserves. - * That same proportion is used to calculate the required amount of pool LP tokens. - * For example, if the required wS tokens is 10% of the pool's wS reserves, - * then 10% of the pool's LP supply needs to be burned. - * - * Because we are doing balanced removal we should be making profit when removing liquidity in a - * pool tilted to either side. - * - * Important: A downside is that the Strategist / Governor needs to be - * cognizant of not removing too much liquidity. And while the proposal to remove liquidity - * is being voted on, the pool tilt might change so much that the proposal that has been valid while - * created is no longer valid. - */ - - (uint256 wsReserves, , ) = IPair(pool).getReserves(); - require(wsReserves > 0, "Empty pool"); - - lpTokens = (_wsAmount * IPair(pool).totalSupply()) / wsReserves; - lpTokens += 1; // Add 1 to ensure we get enough LP tokens with rounding - } - - /** - * @dev Deposit Wrapped S (wS) and OS liquidity to the SwapX pool - * and stake the pool's LP token in the gauge. - * @param _wsAmount Amount of Wrapped S (wS) to deposit. - * @param _osAmount Amount of OS to deposit. - * @return lpTokens Amount of SwapX pool LP tokens minted. - */ - function _depositToPoolAndGauge(uint256 _wsAmount, uint256 _osAmount) - internal - returns (uint256 lpTokens) - { - // Transfer wS to the pool - IERC20(ws).safeTransfer(pool, _wsAmount); - // Transfer OS to the pool - IERC20(os).safeTransfer(pool, _osAmount); - - // Mint LP tokens from the pool - lpTokens = IPair(pool).mint(address(this)); - - // Deposit the pool's LP tokens into the gauge - IGauge(gauge).deposit(lpTokens); - } - - /** - * @dev Withdraw pool LP tokens from the gauge and remove wS and OS from the pool. - * @param _lpTokens Amount of SwapX pool LP tokens to withdraw from the gauge - */ - function _withdrawFromGaugeAndPool(uint256 _lpTokens) internal { - require( - IGauge(gauge).balanceOf(address(this)) >= _lpTokens, - "Not enough LP tokens in gauge" - ); - - // Withdraw pool LP tokens from the gauge - IGauge(gauge).withdraw(_lpTokens); - - // Transfer the pool LP tokens to the pool - IERC20(pool).safeTransfer(pool, _lpTokens); - - // Burn the LP tokens and transfer the wS and OS back to the strategy - IPair(pool).burn(address(this)); - } - - /** - * @dev Withdraw all pool LP tokens from the gauge when it's in emergency mode - * and remove wS and OS from the pool. - */ - function _emergencyWithdrawFromGaugeAndPool() internal { - // Withdraw all pool LP tokens from the gauge - IGauge(gauge).emergencyWithdraw(); - - // Get the pool LP tokens in strategy - uint256 _lpTokens = IERC20(pool).balanceOf(address(this)); - - // Transfer the pool LP tokens to the pool - IERC20(pool).safeTransfer(pool, _lpTokens); - - // Burn the LP tokens and transfer the wS and OS back to the strategy - IPair(pool).burn(address(this)); - } - - /** - * @dev Swap exact amount of tokens for another token against the pool. - * @param _amountIn Amount of tokens to swap into the pool. - * @param _tokenIn Address of the token going into the pool. - * @param _tokenOut Address of the token being swapped out of the pool. - */ - function _swapExactTokensForTokens( - uint256 _amountIn, - address _tokenIn, - address _tokenOut - ) internal { - // Transfer in tokens to the pool - IERC20(_tokenIn).safeTransfer(pool, _amountIn); - - // Calculate how much out tokens we get from the swap - uint256 amountOut = IPair(pool).getAmountOut(_amountIn, _tokenIn); - - // Safety check that we are dealing with the correct pool tokens - require( - (_tokenIn == ws && _tokenOut == os) || - (_tokenIn == os && _tokenOut == ws), - "Unsupported swap" - ); - - // Work out the correct order of the amounts for the pool - (uint256 amount0, uint256 amount1) = _tokenIn == ws - ? (uint256(0), amountOut) - : (amountOut, 0); - - // Perform the swap on the pool - IPair(pool).swap(amount0, amount1, address(this), new bytes(0)); - - // The slippage protection against the amount out is indirectly done - // via the improvePoolBalance - } - - /// @dev Calculate the value of a LP position in a SwapX stable pool - /// if the pool was balanced. - /// @param _lpTokens Amount of LP tokens in the SwapX pool - /// @return value The wS value of the LP tokens when the pool is balanced - function _lpValue(uint256 _lpTokens) internal view returns (uint256 value) { - // Get total supply of LP tokens - uint256 totalSupply = IPair(pool).totalSupply(); - if (totalSupply == 0) return 0; - - // Get the current reserves of the pool - (uint256 wsReserves, uint256 osReserves, ) = IPair(pool).getReserves(); - - // Calculate the invariant of the pool assuming both tokens have 18 decimals. - // k is scaled to 18 decimals. - uint256 k = _invariant(wsReserves, osReserves); - - // If x = y, let’s denote x = y = z (where z is the common reserve value) - // Substitute z into the invariant: - // k = z^3 * z + z * z^3 - // k = 2 * z^4 - // Going back the other way to calculate the common reserve value z - // z = (k / 2) ^ (1/4) - // the total value of the pool when x = y is 2 * z, which is 2 * (k / 2) ^ (1/4) - uint256 zSquared = sqrt((k * 1e18) / 2); // 18 + 18 = 36 decimals becomes 18 decimals after sqrt - uint256 z = sqrt(zSquared * 1e18); // 18 + 18 = 36 decimals becomes 18 decimals after sqrt - uint256 totalValueOfPool = 2 * z; - - // lp value = lp tokens * value of pool / total supply - value = (_lpTokens * totalValueOfPool) / totalSupply; - } - - /** - * @dev Compute the invariant for a SwapX stable pool. - * This assumed both x and y tokens are to 18 decimals which is checked in the constructor. - * invariant: k = x^3 * y + x * y^3 - * @dev This implementation is copied from SwapX's Pair contract. - * @param _x The amount of Wrapped S (wS) tokens in the pool - * @param _y The amount of the OS tokens in the pool - * @return k The invariant of the SwapX stable pool - */ - function _invariant(uint256 _x, uint256 _y) - internal - pure - returns (uint256 k) - { - uint256 _a = (_x * _y) / PRECISION; - uint256 _b = ((_x * _x) / PRECISION + (_y * _y) / PRECISION); - // slither-disable-next-line divide-before-multiply - k = (_a * _b) / PRECISION; - } - - /** - * @dev Checks that the protocol is solvent, protecting from a rogue Strategist / Guardian that can - * keep rebalancing the pool in both directions making the protocol lose a tiny amount of - * funds each time. - * - * Protocol must be at least SOLVENCY_THRESHOLD (99,8 %) backed in order for the rebalances to - * function. - */ - function _solvencyAssert() internal view { - uint256 _totalVaultValue = IVault(vaultAddress).totalValue(); - uint256 _totalSupply = IERC20(os).totalSupply(); - - if ( - _totalSupply > 0 && - _totalVaultValue.divPrecisely(_totalSupply) < SOLVENCY_THRESHOLD - ) { - revert("Protocol insolvent"); - } - } - - /*************************************** - Setters - ****************************************/ - - /** - * @notice Set the maximum deviation from the OS/wS peg (1e18) before deposits are reverted. - * @param _maxDepeg the OS/wS price from peg (1e18) in 18 decimals. - * eg 0.01e18 or 1e16 is 1% which is 100 basis points. - */ - function setMaxDepeg(uint256 _maxDepeg) external onlyGovernor { - maxDepeg = _maxDepeg; - - emit MaxDepegUpdated(_maxDepeg); - } - - /*************************************** - Approvals - ****************************************/ - - /** - * @notice Approve the spending of all assets by their corresponding pool tokens, - * if for some reason is it necessary. - */ - function safeApproveAllTokens() - external - override - onlyGovernor - nonReentrant - { - _approveBase(); - } - - // solhint-disable-next-line no-unused-vars - function _abstractSetPToken(address _asset, address _pToken) - internal - override - {} - - function _approveBase() internal { - // Approve SwapX gauge contract to transfer SwapX pool LP tokens - // This is needed for deposits of SwapX pool LP tokens into the gauge. - // slither-disable-next-line unused-return - IPair(pool).approve(address(gauge), type(uint256).max); + ) StableSwapAMMStrategy(_baseConfig, _os, _ws, _gauge) { } } From 3c5655e4781f20395f16484f8e08d3cadf6a3226 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 13 Feb 2026 21:48:40 +0100 Subject: [PATCH 04/44] rename the variable in stableswap --- .../algebra/StableSwapAMMStrategy.sol | 288 +++++++++--------- contracts/deploy/deployActions.js | 24 +- contracts/deploy/sonic/009_swapx_amo.js | 4 +- contracts/deploy/sonic/027_upgrade_swapx.js | 32 ++ .../sonic/swapx-amo.sonic.fork-test.js | 29 +- 5 files changed, 218 insertions(+), 159 deletions(-) create mode 100644 contracts/deploy/sonic/027_upgrade_swapx.js diff --git a/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol b/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol index dcbd1df353..e52362afdf 100644 --- a/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol +++ b/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol @@ -79,7 +79,7 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy { } /** - * @dev Skim the SwapX pool in case any extra wS or OS tokens were added + * @dev Skim the Algebra pool in case any extra asset or OToken tokens were added */ modifier skimPool() { IPair(pool).skim(address(this)); @@ -118,7 +118,7 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy { modifier improvePoolBalance() { // Get the asset and OToken balances in the pool (uint256 assetReserveBefore, uint256 oTokenReserveBefore) = _getPoolReserves(); - // diff = wS balance - OS balance + // diff = asset balance - OToken balance int256 diffBefore = assetReserveBefore.toInt256() - oTokenReserveBefore.toInt256(); @@ -146,7 +146,7 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy { } /** - * @param _baseConfig The `platformAddress` is the address of the SwapX pool. + * @param _baseConfig The `platformAddress` is the address of the Algebra pool. * The `vaultAddress` is the address of the Origin Sonic Vault. * @param _oToken Address of the OToken. * @param _asset Address of the asset token. @@ -164,7 +164,7 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy { IBasicToken(_oToken).decimals() == 18, "Incorrect token decimals" ); - // Check the SwapX pool is a Stable AMM (sAMM) + // Check the Algebra pool is a Stable AMM (sAMM) require( IPair(_baseConfig.platformAddress).isStable() == true, "Pool not stable" @@ -194,10 +194,10 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy { /** * Initializer for setting up strategy internal state. This overrides the - * InitializableAbstractStrategy initializer as SwapX strategies don't fit + * InitializableAbstractStrategy initializer as Algebra strategies don't fit * well within that abstraction. * @param _rewardTokenAddresses Array containing SWPx token address - * @param _maxDepeg The max amount the OS/wS price can deviate from peg (1e18) before deposits are reverted. + * @param _maxDepeg The max amount the OToken/asset price can deviate from peg (1e18) before deposits are reverted. */ function initialize( address[] calldata _rewardTokenAddresses, @@ -251,7 +251,7 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy { // Ensure solvency of the vault _solvencyAssert(); - // Emit event for the deposited wS tokens + // Emit event for the deposited asset tokens emit Deposit(asset, pool, _assetAmount); // Emit event for the minted OToken tokens emit Deposit(oToken, pool, oTokenDepositAmount); @@ -294,7 +294,7 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy { * mint the pool's LP token and deposit in the gauge. * @param _assetAmount Amount of asset tokens to deposit. * @return oTokenDepositAmount Amount of OToken tokens minted and deposited into the pool. - * @return lpTokens Amount of SwapX pool LP tokens minted and deposited into the gauge. + * @return lpTokens Amount of Algebra pool LP tokens minted and deposited into the gauge. */ function _deposit(uint256 _assetAmount) internal @@ -315,56 +315,56 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy { ****************************************/ /** - * @notice Withdraw wS and OS from the SwapX pool, burn the OS, - * and transfer the wS to the recipient. + * @notice Withdraw asset and OToken from the Algebra pool, burn the OToken, + * and transfer the asset to the recipient. * @param _recipient Address of the Vault. - * @param _asset Address of the Wrapped S (wS) contract. - * @param _wsAmount Amount of Wrapped S (wS) to withdraw. + * @param _asset Address of the asset token. + * @param _assetAmount Amount of asset tokens to withdraw. */ function withdraw( address _recipient, address _asset, - uint256 _wsAmount + uint256 _assetAmount ) external override onlyVault nonReentrant skimPool { - require(_wsAmount > 0, "Must withdraw something"); + require(_assetAmount > 0, "Must withdraw something"); require(_asset == asset, "Unsupported asset"); - // This strategy can't be set as a default strategy for wS in the Vault. + // This strategy can't be set as a default strategy for asset in the Vault. // This means the recipient must always be the Vault. require(_recipient == vaultAddress, "Only withdraw to vault allowed"); - // Calculate how much pool LP tokens to burn to get the required amount of wS tokens back - uint256 lpTokens = _calcTokensToBurn(_wsAmount); + // Calculate how much pool LP tokens to burn to get the required amount of asset tokens back + uint256 lpTokens = _calcTokensToBurn(_assetAmount); // Withdraw pool LP tokens from the gauge and remove assets from from the pool _withdrawFromGaugeAndPool(lpTokens); - // Burn all the removed OS and any that was left in the strategy - uint256 osToBurn = IERC20(oToken).balanceOf(address(this)); - IVault(vaultAddress).burnForStrategy(osToBurn); + // Burn all the removed OToken and any that was left in the strategy + uint256 oTokenToBurn = IERC20(oToken).balanceOf(address(this)); + IVault(vaultAddress).burnForStrategy(oTokenToBurn); - // Transfer wS to the recipient - // Note there can be a dust amount of wS left in the strategy as + // Transfer asset to the recipient + // Note there can be a dust amount of asset left in the strategy as // the burn of the pool's LP tokens is rounded up require( - IERC20(asset).balanceOf(address(this)) >= _wsAmount, - "Not enough wS removed from pool" + IERC20(asset).balanceOf(address(this)) >= _assetAmount, + "Not enough asset removed" ); - IERC20(asset).safeTransfer(_recipient, _wsAmount); + IERC20(asset).safeTransfer(_recipient, _assetAmount); // Ensure solvency of the vault _solvencyAssert(); - // Emit event for the withdrawn wS tokens - emit Withdrawal(asset, pool, _wsAmount); - // Emit event for the burnt OS tokens - emit Withdrawal(oToken, pool, osToBurn); + // Emit event for the withdrawn asset tokens + emit Withdrawal(asset, pool, _assetAmount); + // Emit event for the burnt OToken tokens + emit Withdrawal(oToken, pool, oTokenToBurn); } /** * @notice Withdraw all pool LP tokens from the gauge, - * remove all wS and OS from the SwapX pool, - * burn all the OS tokens, - * and transfer all the wS to the Vault contract. + * remove all asset and OToken from the Algebra pool, + * burn all the OToken, + * and transfer all the asset to the Vault contract. * @dev There is no solvency check here as withdrawAll can be called to * quickly secure assets to the Vault in emergencies. */ @@ -388,112 +388,112 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy { _withdrawFromGaugeAndPool(lpTokens); } - // Burn all OS in this strategy contract - uint256 osToBurn = IERC20(oToken).balanceOf(address(this)); - IVault(vaultAddress).burnForStrategy(osToBurn); + // Burn all OToken in this strategy contract + uint256 oTokenToBurn = IERC20(oToken).balanceOf(address(this)); + IVault(vaultAddress).burnForStrategy(oTokenToBurn); - // Get the strategy contract's wS balance. - // This includes all that was removed from the SwapX pool and + // Get the strategy contract's asset balance. + // This includes all that was removed from the Algebra pool and // any that was sitting in the strategy contract before the removal. - uint256 wsBalance = IERC20(asset).balanceOf(address(this)); - IERC20(asset).safeTransfer(vaultAddress, wsBalance); + uint256 assetBalance = IERC20(asset).balanceOf(address(this)); + IERC20(asset).safeTransfer(vaultAddress, assetBalance); - // Emit event for the withdrawn wS tokens - emit Withdrawal(asset, pool, wsBalance); - // Emit event for the burnt OS tokens - emit Withdrawal(oToken, pool, osToBurn); + // Emit event for the withdrawn asset tokens + emit Withdrawal(asset, pool, assetBalance); + // Emit event for the burnt OToken tokens + emit Withdrawal(oToken, pool, oTokenToBurn); } /*************************************** Pool Rebalancing ****************************************/ - /** @notice Used when there is more OS than wS in the pool. - * wS and OS is removed from the pool, the received wS is swapped for OS - * and the left over OS in the strategy is burnt. - * The OS/wS price is < 1.0 so OS is being bought at a discount. - * @param _wsAmount Amount of Wrapped S (wS) to swap into the pool. + /** @notice Used when there is more OToken than asset in the pool. + * asset and OToken is removed from the pool, the received asset is swapped for OToken + * and the left over OToken in the strategy is burnt. + * The OToken/asset price is < 1.0 so OToken is being bought at a discount. + * @param _assetAmount Amount of asset tokens to swap into the pool. */ - function swapAssetsToPool(uint256 _wsAmount) + function swapAssetsToPool(uint256 _assetAmount) external onlyStrategist nonReentrant improvePoolBalance skimPool { - require(_wsAmount > 0, "Must swap something"); + require(_assetAmount > 0, "Must swap something"); - // 1. Partially remove liquidity so there’s enough wS for the swap + // 1. Partially remove liquidity so there’s enough asset for the swap - // Calculate how much pool LP tokens to burn to get the required amount of wS tokens back - uint256 lpTokens = _calcTokensToBurn(_wsAmount); + // Calculate how much pool LP tokens to burn to get the required amount of asset tokens back + uint256 lpTokens = _calcTokensToBurn(_assetAmount); require(lpTokens > 0, "No LP tokens to burn"); _withdrawFromGaugeAndPool(lpTokens); - // 2. Swap wS for OS against the pool - // Swap exact amount of wS for OS against the pool - // There can be a dust amount of wS left in the strategy as the burn of the pool's LP tokens is rounded up - _swapExactTokensForTokens(_wsAmount, asset, oToken); + // 2. Swap asset for OToken against the pool + // Swap exact amount of asset for OToken against the pool + // There can be a dust amount of asset left in the strategy as the burn of the pool's LP tokens is rounded up + _swapExactTokensForTokens(_assetAmount, asset, oToken); - // 3. Burn all the OS left in the strategy from the remove liquidity and swap - uint256 osToBurn = IERC20(oToken).balanceOf(address(this)); - IVault(vaultAddress).burnForStrategy(osToBurn); + // 3. Burn all the OToken left in the strategy from the remove liquidity and swap + uint256 oTokenToBurn = IERC20(oToken).balanceOf(address(this)); + IVault(vaultAddress).burnForStrategy(oTokenToBurn); // Ensure solvency of the vault _solvencyAssert(); - // Emit event for the burnt OS tokens - emit Withdrawal(oToken, pool, osToBurn); + // Emit event for the burnt OToken tokens + emit Withdrawal(oToken, pool, oTokenToBurn); // Emit event for the swap - emit SwapAssetsToPool(_wsAmount, lpTokens, osToBurn); + emit SwapAssetsToPool(_assetAmount, lpTokens, oTokenToBurn); } /** - * @notice Used when there is more wS than OS in the pool. - * OS is minted and swapped for wS against the pool, - * more OS is minted and added back into the pool with the swapped out wS. - * The OS/wS price is > 1.0 so OS is being sold at a premium. - * @param _osAmount Amount of OS to swap into the pool. + * @notice Used when there is more asset than OToken in the pool. + * OToken is minted and swapped for asset against the pool, + * more OToken is minted and added back into the pool with the swapped out asset. + * The OToken/asset price is > 1.0 so OToken is being sold at a premium. + * @param _oTokenAmount Amount of OToken to swap into the pool. */ - function swapOTokensToPool(uint256 _osAmount) + function swapOTokensToPool(uint256 _oTokenAmount) external onlyStrategist nonReentrant improvePoolBalance skimPool { - require(_osAmount > 0, "Must swap something"); + require(_oTokenAmount > 0, "Must swap something"); - // 1. Mint OS so it can be swapped into the pool + // 1. Mint OToken so it can be swapped into the pool - // There can be OS in the strategy from skimming the pool - uint256 osInStrategy = IERC20(oToken).balanceOf(address(this)); - require(_osAmount >= osInStrategy, "Too much OS in strategy"); - uint256 osToMint = _osAmount - osInStrategy; + // There can be OToken in the strategy from skimming the pool + uint256 oTokenInStrategy = IERC20(oToken).balanceOf(address(this)); + require(_oTokenAmount >= oTokenInStrategy, "Too much OToken in strategy"); + uint256 oTokenToMint = _oTokenAmount - oTokenInStrategy; - // Mint the required OS tokens to this strategy - IVault(vaultAddress).mintForStrategy(osToMint); + // Mint the required OToken tokens to this strategy + IVault(vaultAddress).mintForStrategy(oTokenToMint); - // 2. Swap OS for wS against the pool - _swapExactTokensForTokens(_osAmount, oToken, asset); + // 2. Swap OToken for asset against the pool + _swapExactTokensForTokens(_oTokenAmount, oToken, asset); - // The wS is from the swap and any wS that was sitting in the strategy - uint256 wsDepositAmount = IERC20(asset).balanceOf(address(this)); + // The asset is from the swap and any asset that was sitting in the strategy + uint256 assetDepositAmount = IERC20(asset).balanceOf(address(this)); - // 3. Add wS and OS back to the pool in proportion to the pool's reserves - (uint256 osDepositAmount, uint256 lpTokens) = _deposit(wsDepositAmount); + // 3. Add asset and OToken back to the pool in proportion to the pool's reserves + (uint256 oTokenDepositAmount, uint256 lpTokens) = _deposit(assetDepositAmount); // Ensure solvency of the vault _solvencyAssert(); - // Emit event for the minted OS tokens - emit Deposit(oToken, pool, osToMint + osDepositAmount); + // Emit event for the minted OToken tokens + emit Deposit(oToken, pool, oTokenToMint + oTokenDepositAmount); // Emit event for the swap emit SwapOTokensToPool( - osToMint, - wsDepositAmount, - osDepositAmount, + oTokenToMint, + assetDepositAmount, + oTokenDepositAmount, lpTokens ); } @@ -503,11 +503,11 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy { ****************************************/ /** - * @notice Get the wS value of assets in the strategy and SwapX pool. + * @notice Get the asset value of assets in the strategy and Algebra pool. * The value of the assets in the pool is calculated assuming the pool is balanced. * This way the value can not be manipulated by changing the pool's token balances. - * @param _asset Address of the Wrapped S (wS) token - * @return balance Total value in wS. + * @param _asset Address of the asset token + * @return balance Total value in asset. */ function checkBalance(address _asset) external @@ -517,14 +517,14 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy { { require(_asset == asset, "Unsupported asset"); - // wS balance needed here for the balance check that happens from vault during depositing. + // asset balance needed here for the balance check that happens from vault during depositing. balance = IERC20(asset).balanceOf(address(this)); // This assumes 1 gauge LP token = 1 pool LP token uint256 lpTokens = IGauge(gauge).balanceOf(address(this)); if (lpTokens == 0) return balance; - // Add the strategy’s share of the wS and OS tokens in the SwapX pool if the pool was balanced. + // Add the strategy’s share of the asset and OToken tokens in the Algebra pool if the pool was balanced. balance += _lpValue(lpTokens); } @@ -552,44 +552,44 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy { } /*************************************** - Internal SwapX Pool and Gauge Functions + Internal Algebra Pool and Gauge Functions ****************************************/ /** - * @dev Calculate the required amount of OS to mint based on the wS amount. - * This ensures the proportion of OS tokens being added to the pool matches the proportion of wS tokens. - * For example, if the added wS tokens is 10% of existing wS tokens in the pool, - * then the OS tokens being added should also be 10% of the OS tokens in the pool. - * @param _wsAmount Amount of Wrapped S (wS) to be added to the pool. - * @return osAmount Amount of OS to be minted and added to the pool. + * @dev Calculate the required amount of OToken to mint based on the asset amount. + * This ensures the proportion of OToken tokens being added to the pool matches the proportion of asset tokens. + * For example, if the added asset tokens is 10% of existing asset tokens in the pool, + * then the OToken tokens being added should also be 10% of the OToken tokens in the pool. + * @param _assetAmount Amount of asset tokens to be added to the pool. + * @return oTokenAmount Amount of OToken tokens to be minted and added to the pool. */ - function _calcTokensToMint(uint256 _wsAmount) + function _calcTokensToMint(uint256 _assetAmount) internal view - returns (uint256 osAmount) + returns (uint256 oTokenAmount) { - (uint256 wsReserves, uint256 osReserves, ) = IPair(pool).getReserves(); - require(wsReserves > 0, "Empty pool"); + (uint256 assetReserves, uint256 oTokenReserves) = _getPoolReserves(); + require(assetReserves > 0, "Empty pool"); - // OS to add = (wS being added * OS in pool) / wS in pool - osAmount = (_wsAmount * osReserves) / wsReserves; + // OToken to add = (asset being added * OToken in pool) / asset in pool + oTokenAmount = (_assetAmount * oTokenReserves) / assetReserves; } /** - * @dev Calculate how much pool LP tokens to burn to get the required amount of wS tokens back + * @dev Calculate how much pool LP tokens to burn to get the required amount of asset tokens back * from the pool. - * @param _wsAmount Amount of Wrapped S (wS) to be removed from the pool. - * @return lpTokens Amount of SwapX pool LP tokens to burn. + * @param _assetAmount Amount of asset tokens to be removed from the pool. + * @return lpTokens Amount of Algebra pool LP tokens to burn. */ - function _calcTokensToBurn(uint256 _wsAmount) + function _calcTokensToBurn(uint256 _assetAmount) internal view returns (uint256 lpTokens) { - /* The SwapX pool proportionally returns the reserve tokens when removing liquidity. - * First, calculate the proportion of required wS tokens against the pools wS reserves. + /* The Algebra pool proportionally returns the reserve tokens when removing liquidity. + * First, calculate the proportion of required asset tokens against the pools asset reserves. * That same proportion is used to calculate the required amount of pool LP tokens. - * For example, if the required wS tokens is 10% of the pool's wS reserves, + * For example, if the required asset tokens is 10% of the pool's asset reserves, * then 10% of the pool's LP supply needs to be burned. * * Because we are doing balanced removal we should be making profit when removing liquidity in a @@ -601,28 +601,28 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy { * created is no longer valid. */ - (uint256 wsReserves, , ) = IPair(pool).getReserves(); - require(wsReserves > 0, "Empty pool"); + (uint256 assetReserves, ) = _getPoolReserves(); + require(assetReserves > 0, "Empty pool"); - lpTokens = (_wsAmount * IPair(pool).totalSupply()) / wsReserves; + lpTokens = (_assetAmount * IPair(pool).totalSupply()) / assetReserves; lpTokens += 1; // Add 1 to ensure we get enough LP tokens with rounding } /** - * @dev Deposit Wrapped S (wS) and OS liquidity to the SwapX pool + * @dev Deposit asset and OToken liquidity to the Algebra pool * and stake the pool's LP token in the gauge. - * @param _wsAmount Amount of Wrapped S (wS) to deposit. - * @param _osAmount Amount of OS to deposit. - * @return lpTokens Amount of SwapX pool LP tokens minted. + * @param _assetAmount Amount of asset to deposit. + * @param _oTokenAmount Amount of OToken to deposit. + * @return lpTokens Amount of Algebra pool LP tokens minted. */ - function _depositToPoolAndGauge(uint256 _wsAmount, uint256 _osAmount) + function _depositToPoolAndGauge(uint256 _assetAmount, uint256 _oTokenAmount) internal returns (uint256 lpTokens) { - // Transfer wS to the pool - IERC20(asset).safeTransfer(pool, _wsAmount); - // Transfer OS to the pool - IERC20(oToken).safeTransfer(pool, _osAmount); + // Transfer asset to the pool + IERC20(asset).safeTransfer(pool, _assetAmount); + // Transfer OToken to the pool + IERC20(oToken).safeTransfer(pool, _oTokenAmount); // Mint LP tokens from the pool lpTokens = IPair(pool).mint(address(this)); @@ -632,8 +632,8 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy { } /** - * @dev Withdraw pool LP tokens from the gauge and remove wS and OS from the pool. - * @param _lpTokens Amount of SwapX pool LP tokens to withdraw from the gauge + * @dev Withdraw pool LP tokens from the gauge and remove asset and OToken from the pool. + * @param _lpTokens Amount of Algebra pool LP tokens to withdraw from the gauge */ function _withdrawFromGaugeAndPool(uint256 _lpTokens) internal { require( @@ -647,13 +647,13 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy { // Transfer the pool LP tokens to the pool IERC20(pool).safeTransfer(pool, _lpTokens); - // Burn the LP tokens and transfer the wS and OS back to the strategy + // Burn the LP tokens and transfer the asset and OToken back to the strategy IPair(pool).burn(address(this)); } /** * @dev Withdraw all pool LP tokens from the gauge when it's in emergency mode - * and remove wS and OS from the pool. + * and remove asset and OToken from the pool. */ function _emergencyWithdrawFromGaugeAndPool() internal { // Withdraw all pool LP tokens from the gauge @@ -665,7 +665,7 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy { // Transfer the pool LP tokens to the pool IERC20(pool).safeTransfer(pool, _lpTokens); - // Burn the LP tokens and transfer the wS and OS back to the strategy + // Burn the LP tokens and transfer the asset and OToken back to the strategy IPair(pool).burn(address(this)); } @@ -705,21 +705,21 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy { // via the improvePoolBalance } - /// @dev Calculate the value of a LP position in a SwapX stable pool + /// @dev Calculate the value of a LP position in a Algebra stable pool /// if the pool was balanced. - /// @param _lpTokens Amount of LP tokens in the SwapX pool - /// @return value The wS value of the LP tokens when the pool is balanced + /// @param _lpTokens Amount of LP tokens in the Algebra pool + /// @return value The asset value of the LP tokens when the pool is balanced function _lpValue(uint256 _lpTokens) internal view returns (uint256 value) { // Get total supply of LP tokens uint256 totalSupply = IPair(pool).totalSupply(); if (totalSupply == 0) return 0; // Get the current reserves of the pool - (uint256 wsReserves, uint256 osReserves, ) = IPair(pool).getReserves(); + (uint256 assetReserves, uint256 oTokenReserves) = _getPoolReserves(); // Calculate the invariant of the pool assuming both tokens have 18 decimals. // k is scaled to 18 decimals. - uint256 k = _invariant(wsReserves, osReserves); + uint256 k = _invariant(assetReserves, oTokenReserves); // If x = y, let’s denote x = y = z (where z is the common reserve value) // Substitute z into the invariant: @@ -737,13 +737,13 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy { } /** - * @dev Compute the invariant for a SwapX stable pool. + * @dev Compute the invariant for a Algebra stable pool. * This assumed both x and y tokens are to 18 decimals which is checked in the constructor. * invariant: k = x^3 * y + x * y^3 - * @dev This implementation is copied from SwapX's Pair contract. - * @param _x The amount of Wrapped S (wS) tokens in the pool - * @param _y The amount of the OS tokens in the pool - * @return k The invariant of the SwapX stable pool + * @dev This implementation is copied from Algebra's Pair contract. + * @param _x The amount of asset tokens in the pool + * @param _y The amount of the OToken tokens in the pool + * @return k The invariant of the Algebra stable pool */ function _invariant(uint256 _x, uint256 _y) internal @@ -792,8 +792,8 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy { ****************************************/ /** - * @notice Set the maximum deviation from the OS/wS peg (1e18) before deposits are reverted. - * @param _maxDepeg the OS/wS price from peg (1e18) in 18 decimals. + * @notice Set the maximum deviation from the OToken/asset peg (1e18) before deposits are reverted. + * @param _maxDepeg the OToken/asset price from peg (1e18) in 18 decimals. * eg 0.01e18 or 1e16 is 1% which is 100 basis points. */ function setMaxDepeg(uint256 _maxDepeg) external onlyGovernor { @@ -826,8 +826,8 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy { {} function _approveBase() internal { - // Approve SwapX gauge contract to transfer SwapX pool LP tokens - // This is needed for deposits of SwapX pool LP tokens into the gauge. + // Approve Algebra gauge contract to transfer Algebra pool LP tokens + // This is needed for deposits of Algebra pool LP tokens into the gauge. // slither-disable-next-line unused-return IPair(pool).approve(address(gauge), type(uint256).max); } diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index 73f23feafd..040c2fa016 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -1598,12 +1598,6 @@ const getPlumeContracts = async () => { }; const deploySonicSwapXAMOStrategyImplementation = async () => { - const { deployerAddr } = await getNamedAccounts(); - const sDeployer = await ethers.provider.getSigner(deployerAddr); - - const cSonicSwapXAMOStrategyProxy = await ethers.getContract( - "SonicSwapXAMOStrategyProxy" - ); const cOSonicProxy = await ethers.getContract("OSonicProxy"); const cOSonicVaultProxy = await ethers.getContract("OSonicVaultProxy"); @@ -1617,6 +1611,23 @@ const deploySonicSwapXAMOStrategyImplementation = async () => { addresses.sonic.SwapXWSOS.gauge, ] ); + + return dSonicSwapXAMOStrategy; +}; + +const deploySonicSwapXAMOStrategyImplementationAndInitialize = async () => { + const { deployerAddr } = await getNamedAccounts(); + const sDeployer = await ethers.provider.getSigner(deployerAddr); + + const cSonicSwapXAMOStrategyProxy = await ethers.getContract( + "SonicSwapXAMOStrategyProxy" + ); + const cOSonicProxy = await ethers.getContract("OSonicProxy"); + const cOSonicVaultProxy = await ethers.getContract("OSonicVaultProxy"); + + // Deploy Sonic SwapX AMO Strategy implementation + const dSonicSwapXAMOStrategy = await deploySonicSwapXAMOStrategyImplementation(); + const cSonicSwapXAMOStrategy = await ethers.getContractAt( "SonicSwapXAMOStrategy", cSonicSwapXAMOStrategyProxy.address @@ -2110,6 +2121,7 @@ module.exports = { deployPlumeMockRoosterAMOStrategyImplementation, getPlumeContracts, deploySonicSwapXAMOStrategyImplementation, + deploySonicSwapXAMOStrategyImplementationAndInitialize, deployOETHSupernovaAMOStrategyImplementation, deployOETHSupernovaAMOStrategyPoolAndGauge, deployProxyWithCreateX, diff --git a/contracts/deploy/sonic/009_swapx_amo.js b/contracts/deploy/sonic/009_swapx_amo.js index c718d51a95..458a5a7168 100644 --- a/contracts/deploy/sonic/009_swapx_amo.js +++ b/contracts/deploy/sonic/009_swapx_amo.js @@ -4,7 +4,7 @@ const { withConfirmation, } = require("../../utils/deploy"); const { - deploySonicSwapXAMOStrategyImplementation, + deploySonicSwapXAMOStrategyImplementationAndInitialize, } = require("../deployActions"); const addresses = require("../../utils/addresses"); @@ -74,7 +74,7 @@ module.exports = deployOnSonic( // Deploy Sonic SwapX AMO Strategy implementation const cSonicSwapXAMOStrategy = - await deploySonicSwapXAMOStrategyImplementation(); + await deploySonicSwapXAMOStrategyImplementationAndInitialize(); return { actions: [ diff --git a/contracts/deploy/sonic/027_upgrade_swapx.js b/contracts/deploy/sonic/027_upgrade_swapx.js new file mode 100644 index 0000000000..f248a26fc0 --- /dev/null +++ b/contracts/deploy/sonic/027_upgrade_swapx.js @@ -0,0 +1,32 @@ +const { deployOnSonic } = require("../../utils/deploy-l2"); +const { + deploySonicSwapXAMOStrategyImplementation, +} = require("../deployActions"); +const addresses = require("../../utils/addresses"); + +module.exports = deployOnSonic( + { + deployName: "027_upgrade_swapx", + forceSkip: true, + }, + async ({ ethers }) => { + const cSonicSwapXAMOStrategyProxy = await ethers.getContract( + "SonicSwapXAMOStrategyProxy" + ); + + // Deploy Sonic SwapX AMO Strategy implementation + const cSonicSwapXAMOImpl = + await deploySonicSwapXAMOStrategyImplementation(); + + return { + actions: [ + // 1. Upgrade SwapX AMO Strategy + { + contract: cSonicSwapXAMOStrategyProxy, + signature: "upgradeTo(address)", + args: [cSonicSwapXAMOImpl.address], + }, + ], + }; + } +); diff --git a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js index 0945b59fe2..0d8468e5f1 100644 --- a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js +++ b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js @@ -10,7 +10,7 @@ const { setERC20TokenBalance } = require("../../_fund"); const log = require("../../../utils/logger")("test:fork:sonic:swapx:amo"); -describe("Sonic ForkTest: SwapX AMO Strategy", function () { +describe.only("Sonic ForkTest: SwapX AMO Strategy", function () { // Retry up to 3 times on CI this.retries(isCI ? 3 : 0); @@ -27,8 +27,8 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { expect(await swapXAMOStrategy.SOLVENCY_THRESHOLD()).to.equal( parseUnits("0.998", 18) ); - expect(await swapXAMOStrategy.ws()).to.equal(addresses.sonic.wS); - expect(await swapXAMOStrategy.os()).to.equal(addresses.sonic.OSonicProxy); + expect(await swapXAMOStrategy.asset()).to.equal(addresses.sonic.wS); + expect(await swapXAMOStrategy.oToken()).to.equal(addresses.sonic.OSonicProxy); expect(await swapXAMOStrategy.pool()).to.equal( addresses.sonic.SwapXWSOS.pool ); @@ -110,7 +110,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { }); }); - describe("with wS in the vault", () => { + describe.only("with wS in the vault", () => { const loadFixture = createFixtureLoader(swapXAMOFixture, { wsMintAmount: 5000000, depositToStrategy: false, @@ -119,7 +119,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { beforeEach(async () => { fixture = await loadFixture(); }); - it("Vault should deposit wS to AMO strategy", async function () { + it.only("Vault should deposit wS to AMO strategy", async function () { await assertDeposit(parseUnits("2000")); }); it("Only vault can deposit wS to AMO strategy", async function () { @@ -1179,6 +1179,9 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { delta.stratBalance ); log(`Expected strategy balance: ${formatUnits(expectedStratBalance)}`); + console.log("swapXAMOStrategy.checkBalance(wS.address)", formatUnits(await swapXAMOStrategy.checkBalance(wS.address))) + console.log("expectedStratBalance", formatUnits(expectedStratBalance)) + expect(await swapXAMOStrategy.checkBalance(wS.address)).to.withinRange( expectedStratBalance.sub(15), expectedStratBalance.add(15), @@ -1270,14 +1273,21 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { `\nAfter depositing ${formatUnits(wsDepositAmount)} wS to strategy` ); await logProfit(dataBefore); + + const receipt = await tx.wait(); + console.log("receipt", receipt.events); + + console.log("wS.address", wS.address); + console.log("swapXPool.address", swapXPool.address); + console.log("wsDepositAmount", wsDepositAmount); // Check emitted events await expect(tx) .to.emit(swapXAMOStrategy, "Deposit") - .withArgs(wS.address, swapXPool.address, wsDepositAmount); + //.withArgs(wS.address, swapXPool.address, wsDepositAmount); await expect(tx) .to.emit(swapXAMOStrategy, "Deposit") - .withArgs(oSonic.address, swapXPool.address, osMintAmount); + //.withArgs(oSonic.address, swapXPool.address, osMintAmount); // Calculate the value of the wS and OS tokens added to the pool if the pool was balanced const depositValue = calcReserveValue({ @@ -1286,6 +1296,11 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () { }); // log(`Value of deposit: ${formatUnits(depositValue)}`); + console.log("depositValue", formatUnits(depositValue)); + console.log("osMintAmount", formatUnits(osMintAmount)); + console.log("wsDepositAmount", formatUnits(wsDepositAmount)); + console.log("lpMintAmount", formatUnits(lpMintAmount)); + await assertChangedData(dataBefore, { stratBalance: depositValue, osSupply: osMintAmount, From 8e408376d392290e238d95e60c042981c9804a91 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Sat, 14 Feb 2026 11:32:42 +0100 Subject: [PATCH 05/44] fix the fork tests and adjust them to refactoring --- .../algebra/StableSwapAMMStrategy.sol | 6 ++--- contracts/deploy/sonic/027_upgrade_swapx.js | 2 +- contracts/test/_fixture-sonic.js | 27 ++++++++++++++++--- .../sonic/swapx-amo.sonic.fork-test.js | 22 +++------------ 4 files changed, 32 insertions(+), 25 deletions(-) diff --git a/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol b/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol index e52362afdf..f4bc9c9ef5 100644 --- a/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol +++ b/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol @@ -44,6 +44,9 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy { /// @notice Address of the Algebra Gauge contract. address public immutable gauge; + /// @notice Index of the OToken in the Algebra pool. + uint256 public immutable oTokenPoolIndex; + /// @notice The max amount the OToken/asset price can deviate from peg (1e18) /// before deposits are reverted scaled to 18 decimals. /// eg 0.01e18 or 1e16 is 1% which is 100 basis points. @@ -51,9 +54,6 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy { /// allows a price range from 0.995 to 1.005. uint256 public maxDepeg; - /// @notice Index of the OToken in the Algebra pool. - uint256 public oTokenPoolIndex; - event SwapOTokensToPool( uint256 oTokenMinted, uint256 assetDepositAmount, diff --git a/contracts/deploy/sonic/027_upgrade_swapx.js b/contracts/deploy/sonic/027_upgrade_swapx.js index f248a26fc0..47b873cd09 100644 --- a/contracts/deploy/sonic/027_upgrade_swapx.js +++ b/contracts/deploy/sonic/027_upgrade_swapx.js @@ -7,7 +7,7 @@ const addresses = require("../../utils/addresses"); module.exports = deployOnSonic( { deployName: "027_upgrade_swapx", - forceSkip: true, + forceSkip: false, }, async ({ ethers }) => { const cSonicSwapXAMOStrategyProxy = await ethers.getContract( diff --git a/contracts/test/_fixture-sonic.js b/contracts/test/_fixture-sonic.js index 0d320dc748..963f170b6b 100644 --- a/contracts/test/_fixture-sonic.js +++ b/contracts/test/_fixture-sonic.js @@ -1,6 +1,6 @@ const hre = require("hardhat"); const { ethers } = hre; -const { parseUnits } = ethers.utils; +const { parseUnits, formatUnits } = ethers.utils; const mocha = require("mocha"); const hhHelpers = require("@nomicfoundation/hardhat-network-helpers"); @@ -237,7 +237,7 @@ async function swapXAMOFixture( ) { const fixture = await defaultSonicFixture(); - const { oSonic, oSonicVault, rafael, nick, strategist, wS } = fixture; + const { oSonic, oSonicVault, rafael, nick, strategist, governor, wS } = fixture; let swapXAMOStrategy, swapXPool, swapXGauge, swpx; @@ -271,22 +271,43 @@ async function swapXAMOFixture( // Calculate how much to mint based on the wS in the vault, // the withdrawal queue, and the wS to be sent to the strategy - const wsBalance = await wS.balanceOf(oSonicVault.address); + let wsBalance = await wS.balanceOf(oSonicVault.address); + const autoAllocateThreshold = await oSonicVault.autoAllocateThreshold(); const queue = await oSonicVault.withdrawalQueueMetadata(); const available = wsBalance.add(queue.claimed).sub(queue.queued); const mintAmount = wsAmount.sub(available).mul(10); + log(`To deposit ${formatUnits(wsAmount)} wS to the strategy, ${formatUnits(mintAmount)} wS needs to be minted to the vault`); + log(`Vualt has ${formatUnits(wsBalance)} wS in balance and ${formatUnits(available)} wS available considering async withdrawals`); + if (mintAmount.gt(0)) { + log(`Minting ${formatUnits(mintAmount)} wS to the vault and vault`); + // Approve the Vault to transfer wS await wS.connect(nick).approve(oSonicVault.address, mintAmount); + const disableAutoAllocate = autoAllocateThreshold.lt(mintAmount); + + // disable auto allocate for the next mint + if (disableAutoAllocate) { + log(`Mint would trigger auto allocate. Disabling auto allocate for the next mint`); + await oSonicVault.connect(governor).setAutoAllocateThreshold(mintAmount.add(1)); + } // Mint OS with wS // This will sit in the vault, not the strategy await oSonicVault.connect(nick).mint(wS.address, mintAmount, 0); + + // revert the auto allocate setting + if (disableAutoAllocate) { + log(`Setting auto allocate back to the previous value: ${formatUnits(autoAllocateThreshold)} wS`); + await oSonicVault.connect(governor).setAutoAllocateThreshold(autoAllocateThreshold); + } } // Add ETH to the Metapool if (config?.depositToStrategy) { + wsBalance = await wS.balanceOf(oSonicVault.address); + log(`Depositing ${formatUnits(wsAmount)} wS to the strategy. Vault ${formatUnits(wsBalance)} wS balance`); // The strategist deposits the WETH to the AMO strategy await oSonicVault .connect(strategist) diff --git a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js index 0d8468e5f1..a5ce46b16d 100644 --- a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js +++ b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js @@ -110,7 +110,7 @@ describe.only("Sonic ForkTest: SwapX AMO Strategy", function () { }); }); - describe.only("with wS in the vault", () => { + describe("with wS in the vault", () => { const loadFixture = createFixtureLoader(swapXAMOFixture, { wsMintAmount: 5000000, depositToStrategy: false, @@ -119,7 +119,7 @@ describe.only("Sonic ForkTest: SwapX AMO Strategy", function () { beforeEach(async () => { fixture = await loadFixture(); }); - it.only("Vault should deposit wS to AMO strategy", async function () { + it("Vault should deposit wS to AMO strategy", async function () { await assertDeposit(parseUnits("2000")); }); it("Only vault can deposit wS to AMO strategy", async function () { @@ -1179,8 +1179,6 @@ describe.only("Sonic ForkTest: SwapX AMO Strategy", function () { delta.stratBalance ); log(`Expected strategy balance: ${formatUnits(expectedStratBalance)}`); - console.log("swapXAMOStrategy.checkBalance(wS.address)", formatUnits(await swapXAMOStrategy.checkBalance(wS.address))) - console.log("expectedStratBalance", formatUnits(expectedStratBalance)) expect(await swapXAMOStrategy.checkBalance(wS.address)).to.withinRange( expectedStratBalance.sub(15), @@ -1273,13 +1271,6 @@ describe.only("Sonic ForkTest: SwapX AMO Strategy", function () { `\nAfter depositing ${formatUnits(wsDepositAmount)} wS to strategy` ); await logProfit(dataBefore); - - const receipt = await tx.wait(); - console.log("receipt", receipt.events); - - console.log("wS.address", wS.address); - console.log("swapXPool.address", swapXPool.address); - console.log("wsDepositAmount", wsDepositAmount); // Check emitted events await expect(tx) @@ -1294,12 +1285,7 @@ describe.only("Sonic ForkTest: SwapX AMO Strategy", function () { ws: wsDepositAmount, os: osMintAmount, }); - // log(`Value of deposit: ${formatUnits(depositValue)}`); - - console.log("depositValue", formatUnits(depositValue)); - console.log("osMintAmount", formatUnits(osMintAmount)); - console.log("wsDepositAmount", formatUnits(wsDepositAmount)); - console.log("lpMintAmount", formatUnits(lpMintAmount)); + log(`Value of deposit: ${formatUnits(depositValue)}`); await assertChangedData(dataBefore, { stratBalance: depositValue, @@ -1556,7 +1542,7 @@ describe.only("Sonic ForkTest: SwapX AMO Strategy", function () { // Check emitted event await expect(tx) .emit(swapXAMOStrategy, "SwapOTokensToPool") - .withNamedArgs({ osMinted: osAmount }); + .withNamedArgs({ oTokenMinted: osAmount }); await logSnapData(await snapData(), "\nAfter swapping OTokens to the pool"); await logProfit(dataBefore); From 4a9d40a9bc9540be3cd00009c5b9c7365794f865 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Sat, 14 Feb 2026 11:38:43 +0100 Subject: [PATCH 06/44] fix fork tests --- .../algebra/StableSwapAMMStrategy.sol | 38 ++++++-- .../sonic/SonicSwapXAMOStrategy.sol | 4 +- contracts/deploy/deployActions.js | 90 +++++++++++++------ contracts/deploy/mainnet/174_supernova_AMO.js | 12 ++- contracts/deploy/sonic/027_upgrade_swapx.js | 1 - contracts/test/_fixture-sonic.js | 41 +++++++-- contracts/test/_fixture.js | 4 +- .../sonic/swapx-amo.sonic.fork-test.js | 18 ++-- contracts/utils/addresses.js | 3 +- 9 files changed, 142 insertions(+), 69 deletions(-) diff --git a/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol b/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol index f4bc9c9ef5..fc3b99c02e 100644 --- a/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol +++ b/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol @@ -117,7 +117,10 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy { */ modifier improvePoolBalance() { // Get the asset and OToken balances in the pool - (uint256 assetReserveBefore, uint256 oTokenReserveBefore) = _getPoolReserves(); + ( + uint256 assetReserveBefore, + uint256 oTokenReserveBefore + ) = _getPoolReserves(); // diff = asset balance - OToken balance int256 diffBefore = assetReserveBefore.toInt256() - oTokenReserveBefore.toInt256(); @@ -125,7 +128,10 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy { _; // Get the asset and OToken balances in the pool - (uint256 assetReserveAfter, uint256 oTokenReserveAfter) = _getPoolReserves(); + ( + uint256 assetReserveAfter, + uint256 oTokenReserveAfter + ) = _getPoolReserves(); // diff = asset balance - OToken balance int256 diffAfter = assetReserveAfter.toInt256() - oTokenReserveAfter.toInt256(); @@ -174,11 +180,15 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy { IGauge(_gauge).TOKEN() == _baseConfig.platformAddress, "Incorrect gauge" ); - oTokenPoolIndex = IPair(_baseConfig.platformAddress).token0() == _oToken ? 0 : 1; + oTokenPoolIndex = IPair(_baseConfig.platformAddress).token0() == _oToken + ? 0 + : 1; // Check the pool tokens are correct require( - IPair(_baseConfig.platformAddress).token0() == (oTokenPoolIndex == 0 ? _oToken : _asset) && - IPair(_baseConfig.platformAddress).token1() == (oTokenPoolIndex == 0 ? _asset : _oToken), + IPair(_baseConfig.platformAddress).token0() == + (oTokenPoolIndex == 0 ? _oToken : _asset) && + IPair(_baseConfig.platformAddress).token1() == + (oTokenPoolIndex == 0 ? _asset : _oToken), "Incorrect pool tokens" ); @@ -469,7 +479,10 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy { // There can be OToken in the strategy from skimming the pool uint256 oTokenInStrategy = IERC20(oToken).balanceOf(address(this)); - require(_oTokenAmount >= oTokenInStrategy, "Too much OToken in strategy"); + require( + _oTokenAmount >= oTokenInStrategy, + "Too much OToken in strategy" + ); uint256 oTokenToMint = _oTokenAmount - oTokenInStrategy; // Mint the required OToken tokens to this strategy @@ -482,7 +495,9 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy { uint256 assetDepositAmount = IERC20(asset).balanceOf(address(this)); // 3. Add asset and OToken back to the pool in proportion to the pool's reserves - (uint256 oTokenDepositAmount, uint256 lpTokens) = _deposit(assetDepositAmount); + (uint256 oTokenDepositAmount, uint256 lpTokens) = _deposit( + assetDepositAmount + ); // Ensure solvency of the vault _solvencyAssert(); @@ -777,16 +792,21 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy { } /** - * @dev Get the reserves of the pool no matter the order of tokens in the underlying + * @dev Get the reserves of the pool no matter the order of tokens in the underlying * Algebra pool. * @return assetReserves The reserves of the asset token in the pool. * @return oTokenReserves The reserves of the OToken token in the pool. */ - function _getPoolReserves() internal view returns (uint256 assetReserves, uint256 oTokenReserves) { + function _getPoolReserves() + internal + view + returns (uint256 assetReserves, uint256 oTokenReserves) + { (uint256 reserve0, uint256 reserve1, ) = IPair(pool).getReserves(); assetReserves = oTokenPoolIndex == 0 ? reserve1 : reserve0; oTokenReserves = oTokenPoolIndex == 0 ? reserve0 : reserve1; } + /*************************************** Setters ****************************************/ diff --git a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol index b0bfe787a4..f786fa20a2 100644 --- a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol +++ b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol @@ -9,7 +9,6 @@ pragma solidity ^0.8.0; import { StableSwapAMMStrategy } from "../algebra/StableSwapAMMStrategy.sol"; contract SonicSwapXAMOStrategy is StableSwapAMMStrategy { - /** * @param _baseConfig The `platformAddress` is the address of the SwapX pool. * The `vaultAddress` is the address of the Origin Sonic Vault. @@ -22,6 +21,5 @@ contract SonicSwapXAMOStrategy is StableSwapAMMStrategy { address _os, address _ws, address _gauge - ) StableSwapAMMStrategy(_baseConfig, _os, _ws, _gauge) { - } + ) StableSwapAMMStrategy(_baseConfig, _os, _ws, _gauge) {} } diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index 040c2fa016..4d945ebfd7 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -30,7 +30,11 @@ const { const { metapoolLPCRVPid } = require("../utils/constants"); const { replaceContractAt } = require("../utils/hardhat"); const { resolveContract } = require("../utils/resolvers"); -const { impersonateAccount, impersonateAndFund, getSigner } = require("../utils/signers"); +const { + impersonateAccount, + impersonateAndFund, + getSigner, +} = require("../utils/signers"); const { getDefenderSigner } = require("../utils/signersNoHardhat"); const { getTxOpts } = require("../utils/tx"); const createxAbi = require("../abi/createx.json"); @@ -1622,12 +1626,11 @@ const deploySonicSwapXAMOStrategyImplementationAndInitialize = async () => { const cSonicSwapXAMOStrategyProxy = await ethers.getContract( "SonicSwapXAMOStrategyProxy" ); - const cOSonicProxy = await ethers.getContract("OSonicProxy"); - const cOSonicVaultProxy = await ethers.getContract("OSonicVaultProxy"); // Deploy Sonic SwapX AMO Strategy implementation - const dSonicSwapXAMOStrategy = await deploySonicSwapXAMOStrategyImplementation(); - + const dSonicSwapXAMOStrategy = + await deploySonicSwapXAMOStrategyImplementation(); + const cSonicSwapXAMOStrategy = await ethers.getContractAt( "SonicSwapXAMOStrategy", cSonicSwapXAMOStrategyProxy.address @@ -1657,65 +1660,94 @@ const deployOETHSupernovaAMOStrategyPoolAndGauge = async () => { const pairBootstrapper = "0x7F8f2B6D0b0AaE8e95221Ce90B5C26B128C1Cb66"; const topkenHandlerWhitelister = "0xD09A1388F0CcE25DA97E8bBAbf5D083E25a5Fbc6"; const sPairBootstrapper = await impersonateAndFund(pairBootstrapper); - const sTokenHandlerWhitelister = await impersonateAndFund(topkenHandlerWhitelister); + const sTokenHandlerWhitelister = await impersonateAndFund( + topkenHandlerWhitelister + ); const oeth = await ethers.getContract("OETHProxy"); const factoryABI = [ "function createPair(address tokenA, address tokenB, bool stable) external returns (address pair)", - "event PairCreated(address token0, address token1, bool stable, address pair, uint);" + "event PairCreated(address token0, address token1, bool stable, address pair, uint);", ]; const tokenHandlerABI = [ "function whitelistToken(address _token) external", "function whitelistConnector(address _token) external", ]; - const pairCreatedTopic = "0xc4805696c66d7cf352fc1d6bb633ad5ee82f6cb577c453024b6e0eb8306c6fc9"; + const pairCreatedTopic = + "0xc4805696c66d7cf352fc1d6bb633ad5ee82f6cb577c453024b6e0eb8306c6fc9"; const gaugeManagerAbi = [ "function createGauge(address _pool, uint256 _gaugeType) external returns (address _gauge, address _internal_bribe, address _external_bribe)", "function tokenHandler() external view returns (address)", - "event GaugeCreated(address gauge, address creator, address internal_bribe, address external_bribe, address pool)" + "event GaugeCreated(address gauge, address creator, address internal_bribe, address external_bribe, address pool)", ]; - const gaugeManager = await ethers.getContractAt(gaugeManagerAbi, addresses.mainnet.supernovaGaugeManager); - const tokenHandler = await ethers.getContractAt(tokenHandlerABI, await gaugeManager.tokenHandler()); + const gaugeManager = await ethers.getContractAt( + gaugeManagerAbi, + addresses.mainnet.supernovaGaugeManager + ); + const tokenHandler = await ethers.getContractAt( + tokenHandlerABI, + await gaugeManager.tokenHandler() + ); - const factory = await ethers.getContractAt(factoryABI, addresses.mainnet.supernovaPairFactory); + const factory = await ethers.getContractAt( + factoryABI, + addresses.mainnet.supernovaPairFactory + ); let poolAddress; log("Creating new OETH/WETH pair..."); const tx = await factory - .connect(sPairBootstrapper) - .createPair(oeth.address, addresses.mainnet.WETH, true); + .connect(sPairBootstrapper) + .createPair(oeth.address, addresses.mainnet.WETH, true); const receipt = await tx.wait(); - const pairCreatedEvent = receipt.events.find(e => e.topics[0] === pairCreatedTopic); - const [,pairAddress,] = ethers.utils.defaultAbiCoder.decode( + const pairCreatedEvent = receipt.events.find( + (e) => e.topics[0] === pairCreatedTopic + ); + const [, pairAddress] = ethers.utils.defaultAbiCoder.decode( ["bool", "address", "uint256"], pairCreatedEvent.data ); console.log("Pair address:", pairAddress); - + console.log("Whitelisting OETH token & WETH token as connector"); - await tokenHandler.connect(sTokenHandlerWhitelister).whitelistToken(oeth.address); - await tokenHandler.connect(sTokenHandlerWhitelister).whitelistToken(addresses.mainnet.WETH); - await tokenHandler.connect(sTokenHandlerWhitelister).whitelistConnector(addresses.mainnet.WETH); + await tokenHandler + .connect(sTokenHandlerWhitelister) + .whitelistToken(oeth.address); + await tokenHandler + .connect(sTokenHandlerWhitelister) + .whitelistToken(addresses.mainnet.WETH); + await tokenHandler + .connect(sTokenHandlerWhitelister) + .whitelistConnector(addresses.mainnet.WETH); poolAddress = pairAddress; - + log("Creating gauge for OETH/WETH..."); - const gaugeCreatedTopic = ethers.utils.id("GaugeCreated(address,address,address,address,address)"); + const gaugeCreatedTopic = ethers.utils.id( + "GaugeCreated(address,address,address,address,address)" + ); const tx1 = await gaugeManager.createGauge(poolAddress, 0); const receipt1 = await tx1.wait(); - const gaugeCreatedEvent = receipt1.events.find(e => e.topics[0] === gaugeCreatedTopic); - console.log("gaugeCreatedEvent", gaugeCreatedEvent) + const gaugeCreatedEvent = receipt1.events.find( + (e) => e.topics[0] === gaugeCreatedTopic + ); + console.log("gaugeCreatedEvent", gaugeCreatedEvent); //const gaugeAddress = gaugeCreatedEvent.topics[1]; - const gaugeAddress = ethers.utils.getAddress(`0x${gaugeCreatedEvent.topics[1].slice(-40)}`); + const gaugeAddress = ethers.utils.getAddress( + `0x${gaugeCreatedEvent.topics[1].slice(-40)}` + ); log(`Created gauge at ${gaugeAddress}`); - + return { poolAddress, gaugeAddress }; }; -const deployOETHSupernovaAMOStrategyImplementation = async (poolAddress, gaugeAddress) => { +const deployOETHSupernovaAMOStrategyImplementation = async ( + poolAddress, + gaugeAddress +) => { const { deployerAddr } = await getNamedAccounts(); const sDeployer = await ethers.provider.getSigner(deployerAddr); @@ -1725,7 +1757,7 @@ const deployOETHSupernovaAMOStrategyImplementation = async (poolAddress, gaugeAd const cOETHProxy = await ethers.getContract("OETHProxy"); const cOETHVaultProxy = await ethers.getContract("OETHVaultProxy"); - // Deploy Sonic SwapX AMO Strategy implementation that will serve + // Deploy Sonic SwapX AMO Strategy implementation that will serve // OETH Supernova AMO const dSupernovaAMOStrategy = await deployWithConfirmation( "SupernovaAMOStrategy", @@ -1742,7 +1774,7 @@ const deployOETHSupernovaAMOStrategyImplementation = async (poolAddress, gaugeAd "SonicSwapXAMOStrategy", cOETHSupernovaAMOStrategyProxy.address ); - + // Initialize Sonic Curve AMO Strategy implementation const depositPriceRange = parseUnits("0.01", 18); // 1% or 100 basis points const initData = cOETHSupernovaAMOStrategy.interface.encodeFunctionData( diff --git a/contracts/deploy/mainnet/174_supernova_AMO.js b/contracts/deploy/mainnet/174_supernova_AMO.js index f74910b0ac..4b54f4d76f 100644 --- a/contracts/deploy/mainnet/174_supernova_AMO.js +++ b/contracts/deploy/mainnet/174_supernova_AMO.js @@ -5,7 +5,7 @@ const { } = require("../../utils/deploy"); const { deployOETHSupernovaAMOStrategyImplementation, - deployOETHSupernovaAMOStrategyPoolAndGauge + deployOETHSupernovaAMOStrategyPoolAndGauge, } = require("../deployActions"); module.exports = deploymentWithGovernanceProposal( @@ -18,9 +18,10 @@ module.exports = deploymentWithGovernanceProposal( "VaultAdmin", cOETHVaultProxy.address ); - + // TODO: delete once the pools ang gauges are created - const { poolAddress, gaugeAddress } = await deployOETHSupernovaAMOStrategyPoolAndGauge(); + const { poolAddress, gaugeAddress } = + await deployOETHSupernovaAMOStrategyPoolAndGauge(); await deployWithConfirmation("OETHSupernovaAMOProxy"); const cOETHSupernovaAMOProxy = await ethers.getContract( @@ -31,7 +32,10 @@ module.exports = deploymentWithGovernanceProposal( // Deploy Sonic SwapX AMO Strategy implementation const cSupernovaAMOStrategy = - await deployOETHSupernovaAMOStrategyImplementation(poolAddress, gaugeAddress); + await deployOETHSupernovaAMOStrategyImplementation( + poolAddress, + gaugeAddress + ); return { name: "Deploy Supernova AMO Strategy", diff --git a/contracts/deploy/sonic/027_upgrade_swapx.js b/contracts/deploy/sonic/027_upgrade_swapx.js index 47b873cd09..5424deb0c7 100644 --- a/contracts/deploy/sonic/027_upgrade_swapx.js +++ b/contracts/deploy/sonic/027_upgrade_swapx.js @@ -2,7 +2,6 @@ const { deployOnSonic } = require("../../utils/deploy-l2"); const { deploySonicSwapXAMOStrategyImplementation, } = require("../deployActions"); -const addresses = require("../../utils/addresses"); module.exports = deployOnSonic( { diff --git a/contracts/test/_fixture-sonic.js b/contracts/test/_fixture-sonic.js index 963f170b6b..3cf8637b5e 100644 --- a/contracts/test/_fixture-sonic.js +++ b/contracts/test/_fixture-sonic.js @@ -237,7 +237,8 @@ async function swapXAMOFixture( ) { const fixture = await defaultSonicFixture(); - const { oSonic, oSonicVault, rafael, nick, strategist, governor, wS } = fixture; + const { oSonic, oSonicVault, rafael, nick, strategist, governor, wS } = + fixture; let swapXAMOStrategy, swapXPool, swapXGauge, swpx; @@ -277,8 +278,16 @@ async function swapXAMOFixture( const available = wsBalance.add(queue.claimed).sub(queue.queued); const mintAmount = wsAmount.sub(available).mul(10); - log(`To deposit ${formatUnits(wsAmount)} wS to the strategy, ${formatUnits(mintAmount)} wS needs to be minted to the vault`); - log(`Vualt has ${formatUnits(wsBalance)} wS in balance and ${formatUnits(available)} wS available considering async withdrawals`); + log( + `To deposit ${formatUnits(wsAmount)} wS to the strategy, ${formatUnits( + mintAmount + )} wS needs to be minted to the vault` + ); + log( + `Vualt has ${formatUnits(wsBalance)} wS in balance and ${formatUnits( + available + )} wS available considering async withdrawals` + ); if (mintAmount.gt(0)) { log(`Minting ${formatUnits(mintAmount)} wS to the vault and vault`); @@ -287,11 +296,15 @@ async function swapXAMOFixture( await wS.connect(nick).approve(oSonicVault.address, mintAmount); const disableAutoAllocate = autoAllocateThreshold.lt(mintAmount); - + // disable auto allocate for the next mint if (disableAutoAllocate) { - log(`Mint would trigger auto allocate. Disabling auto allocate for the next mint`); - await oSonicVault.connect(governor).setAutoAllocateThreshold(mintAmount.add(1)); + log( + `Mint would trigger auto allocate. Disabling auto allocate for the next mint` + ); + await oSonicVault + .connect(governor) + .setAutoAllocateThreshold(mintAmount.add(1)); } // Mint OS with wS // This will sit in the vault, not the strategy @@ -299,15 +312,25 @@ async function swapXAMOFixture( // revert the auto allocate setting if (disableAutoAllocate) { - log(`Setting auto allocate back to the previous value: ${formatUnits(autoAllocateThreshold)} wS`); - await oSonicVault.connect(governor).setAutoAllocateThreshold(autoAllocateThreshold); + log( + `Setting auto allocate back to the previous value: ${formatUnits( + autoAllocateThreshold + )} wS` + ); + await oSonicVault + .connect(governor) + .setAutoAllocateThreshold(autoAllocateThreshold); } } // Add ETH to the Metapool if (config?.depositToStrategy) { wsBalance = await wS.balanceOf(oSonicVault.address); - log(`Depositing ${formatUnits(wsAmount)} wS to the strategy. Vault ${formatUnits(wsBalance)} wS balance`); + log( + `Depositing ${formatUnits( + wsAmount + )} wS to the strategy. Vault ${formatUnits(wsBalance)} wS balance` + ); // The strategist deposits the WETH to the AMO strategy await oSonicVault .connect(strategist) diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 8f40e8e574..505900443e 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -2569,13 +2569,11 @@ async function instantRebaseVaultFixture(tokenName) { async function supernovaOETHAMOFixure() { const fixture = await defaultFixture(); - const { oeth, weth } = fixture; + //const { oeth, weth } = fixture; return fixture; } - - // Unit test cross chain fixture where both contracts are deployed on the same chain for the // purposes of unit testing async function crossChainFixtureUnit() { diff --git a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js index a5ce46b16d..65c7c4df22 100644 --- a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js +++ b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js @@ -10,7 +10,7 @@ const { setERC20TokenBalance } = require("../../_fund"); const log = require("../../../utils/logger")("test:fork:sonic:swapx:amo"); -describe.only("Sonic ForkTest: SwapX AMO Strategy", function () { +describe("Sonic ForkTest: SwapX AMO Strategy", function () { // Retry up to 3 times on CI this.retries(isCI ? 3 : 0); @@ -28,7 +28,9 @@ describe.only("Sonic ForkTest: SwapX AMO Strategy", function () { parseUnits("0.998", 18) ); expect(await swapXAMOStrategy.asset()).to.equal(addresses.sonic.wS); - expect(await swapXAMOStrategy.oToken()).to.equal(addresses.sonic.OSonicProxy); + expect(await swapXAMOStrategy.oToken()).to.equal( + addresses.sonic.OSonicProxy + ); expect(await swapXAMOStrategy.pool()).to.equal( addresses.sonic.SwapXWSOS.pool ); @@ -1179,7 +1181,7 @@ describe.only("Sonic ForkTest: SwapX AMO Strategy", function () { delta.stratBalance ); log(`Expected strategy balance: ${formatUnits(expectedStratBalance)}`); - + expect(await swapXAMOStrategy.checkBalance(wS.address)).to.withinRange( expectedStratBalance.sub(15), expectedStratBalance.add(15), @@ -1273,12 +1275,10 @@ describe.only("Sonic ForkTest: SwapX AMO Strategy", function () { await logProfit(dataBefore); // Check emitted events - await expect(tx) - .to.emit(swapXAMOStrategy, "Deposit") - //.withArgs(wS.address, swapXPool.address, wsDepositAmount); - await expect(tx) - .to.emit(swapXAMOStrategy, "Deposit") - //.withArgs(oSonic.address, swapXPool.address, osMintAmount); + await expect(tx).to.emit(swapXAMOStrategy, "Deposit") + .withArgs(wS.address, swapXPool.address, wsDepositAmount); + await expect(tx).to.emit(swapXAMOStrategy, "Deposit") + .withArgs(oSonic.address, swapXPool.address, osMintAmount); // Calculate the value of the wS and OS tokens added to the pool if the pool was balanced const depositValue = calcReserveValue({ diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index 4ca806e416..269c71edd7 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -398,8 +398,7 @@ addresses.mainnet.supernovaPairFactory = "0x5aef44edfc5a7edd30826c724ea12d7be15bdc30"; addresses.mainnet.supernovaGaugeManager = "0x19a410046Afc4203AEcE5fbFc7A6Ac1a4F517AE2"; -addresses.mainnet.supernovaToken = - "0x00Da8466B296E382E5Da2Bf20962D0cB87200c78"; +addresses.mainnet.supernovaToken = "0x00Da8466B296E382E5Da2Bf20962D0cB87200c78"; // Mainnet Merkl addresses.mainnet.CampaignCreator = From 681dfdaa3420a522c68ddb7ace4fa0f1013eae48 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Sat, 14 Feb 2026 17:12:53 +0100 Subject: [PATCH 07/44] add strategy file --- .../algebra/OETHSupernovaAMOStrategy.sol | 25 +++++++++++++++++++ contracts/deploy/deployActions.js | 9 +++---- contracts/deploy/sonic/027_upgrade_swapx.js | 4 ++- 3 files changed, 32 insertions(+), 6 deletions(-) create mode 100644 contracts/contracts/strategies/algebra/OETHSupernovaAMOStrategy.sol diff --git a/contracts/contracts/strategies/algebra/OETHSupernovaAMOStrategy.sol b/contracts/contracts/strategies/algebra/OETHSupernovaAMOStrategy.sol new file mode 100644 index 0000000000..1717eb1f7e --- /dev/null +++ b/contracts/contracts/strategies/algebra/OETHSupernovaAMOStrategy.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity ^0.8.0; + +/** + * @title Supernova OETH Algorithmic Market Maker (AMO) Strategy + * @notice AMO strategy for the Supernova OETH/WETH stable pool + * @author Origin Protocol Inc + */ +import { StableSwapAMMStrategy } from "./StableSwapAMMStrategy.sol"; + +contract OETHSupernovaAMOStrategy is StableSwapAMMStrategy { + /** + * @param _baseConfig The `platformAddress` is the address of the Supernova OETH/WETH pool. + * The `vaultAddress` is the address of the OETH Vault. + * @param _oeth Address of the OETH token. + * @param _weth Address of the WETH token. + * @param _gauge Address of the Supernova gauge for the pool. + */ + constructor( + BaseStrategyConfig memory _baseConfig, + address _oeth, + address _weth, + address _gauge + ) StableSwapAMMStrategy(_baseConfig, _oeth, _weth, _gauge) {} +} diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index 4d945ebfd7..2c8584d728 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -1757,21 +1757,20 @@ const deployOETHSupernovaAMOStrategyImplementation = async ( const cOETHProxy = await ethers.getContract("OETHProxy"); const cOETHVaultProxy = await ethers.getContract("OETHVaultProxy"); - // Deploy Sonic SwapX AMO Strategy implementation that will serve + // Deploy OETH Supernova AMO Strategy implementation that will serve // OETH Supernova AMO const dSupernovaAMOStrategy = await deployWithConfirmation( - "SupernovaAMOStrategy", + "OETHSupernovaAMOStrategy", [ [poolAddress, cOETHVaultProxy.address], cOETHProxy.address, addresses.mainnet.WETH, gaugeAddress, - ], - "SonicSwapXAMOStrategy" + ] ); const cOETHSupernovaAMOStrategy = await ethers.getContractAt( - "SonicSwapXAMOStrategy", + "OETHSupernovaAMOStrategy", cOETHSupernovaAMOStrategyProxy.address ); diff --git a/contracts/deploy/sonic/027_upgrade_swapx.js b/contracts/deploy/sonic/027_upgrade_swapx.js index 5424deb0c7..b5c3913a0e 100644 --- a/contracts/deploy/sonic/027_upgrade_swapx.js +++ b/contracts/deploy/sonic/027_upgrade_swapx.js @@ -3,10 +3,12 @@ const { deploySonicSwapXAMOStrategyImplementation, } = require("../deployActions"); +// This is just used to confirm that the Refactoring SwapX AMO strategy into a generalized Algebra strategy is working +// as expected module.exports = deployOnSonic( { deployName: "027_upgrade_swapx", - forceSkip: false, + forceSkip: true, }, async ({ ethers }) => { const cSonicSwapXAMOStrategyProxy = await ethers.getContract( From f7ffaecdb6925b4f3770d86f032ca09335bfff06 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 16 Feb 2026 01:46:11 +0100 Subject: [PATCH 08/44] start re-working tests --- contracts/deploy/sonic/027_upgrade_swapx.js | 2 +- .../test/behaviour/algebraAmoStrategy.js | 1664 +++++++++++++++++ .../sonic/swapx-amo.sonic.fork-test.js | 1653 +--------------- 3 files changed, 1688 insertions(+), 1631 deletions(-) create mode 100644 contracts/test/behaviour/algebraAmoStrategy.js diff --git a/contracts/deploy/sonic/027_upgrade_swapx.js b/contracts/deploy/sonic/027_upgrade_swapx.js index b5c3913a0e..4524f2194f 100644 --- a/contracts/deploy/sonic/027_upgrade_swapx.js +++ b/contracts/deploy/sonic/027_upgrade_swapx.js @@ -8,7 +8,7 @@ const { module.exports = deployOnSonic( { deployName: "027_upgrade_swapx", - forceSkip: true, + forceSkip: false, }, async ({ ethers }) => { const cSonicSwapXAMOStrategyProxy = await ethers.getContract( diff --git a/contracts/test/behaviour/algebraAmoStrategy.js b/contracts/test/behaviour/algebraAmoStrategy.js new file mode 100644 index 0000000000..baf402d8fe --- /dev/null +++ b/contracts/test/behaviour/algebraAmoStrategy.js @@ -0,0 +1,1664 @@ +const { expect } = require("chai"); +const { formatUnits, parseUnits } = require("ethers/lib/utils"); + +const { isCI } = require("../helpers"); + +const log = require("../../utils/logger")("test:fork:algebra:amo"); +/** + * + * @param {*} context a function that returns a fixture with the additional properties: + * - strategy: the strategy to test + * @example + shouldBehaveLikeAlgebraAmoStrategy(() => ({ + assetToken: addresses.sonic.wS, // address of the asset token in the pool + oToken: addresses.sonic.os, // address of the oToken in the pool + amoStrategy: strategy contract, // address of the strategy + pool: pool contract + gauge: addresses.sonic.SwapXWSOS.gauge, // address of the gauge + governor: addresses.sonic.timelock, // address of the governor + timelock: addresses.sonic.timelock, // address of the timelock + strategist: addresses.sonic.strategist, // address of the strategist + nick: addresses.sonic.nick, // nick's address + vaultSigner: addresses.sonic.vaultSigner, // address of the vault signer + })); + */ +const shouldBehaveLikeAlgebraAmoStrategy = (context) => { + describe("ForkTest: Algebra AMO Strategy", function () { + // Retry up to 3 times on CI + this.retries(isCI ? 3 : 0); + let fixture; + describe("post deployment", () => { + beforeEach(async () => { + const { loadFixture } = await context(); + fixture = await loadFixture(); + }); + it("Should have constants and immutables set", async () => { + const { amoStrategy, assetToken, oToken, pool, gauge, governor } = await context(); + + expect(await amoStrategy.SOLVENCY_THRESHOLD()).to.equal( + parseUnits("0.998", 18) + ); + expect(await amoStrategy.asset()).to.equal(assetToken.address); + expect(await amoStrategy.oToken()).to.equal( + oToken.address + ); + expect(await amoStrategy.pool()).to.equal( + pool.address + ); + expect(await amoStrategy.gauge()).to.equal( + gauge.address + ); + expect(await amoStrategy.governor()).to.equal( + governor.address + ); + expect(await amoStrategy.supportsAsset(assetToken.address)).to.true; + expect(await amoStrategy.maxDepeg()).to.equal(parseUnits("0.01")); + }); + it("Should be able to check balance", async () => { + const { assetToken, nick, amoStrategy } = await context(); + + const balance = await amoStrategy.checkBalance(assetToken.address); + log(`check balance ${balance}`); + expect(balance).gte(0); + + // This uses a transaction to call a view function so the gas usage can be reported. + const tx = await amoStrategy + .connect(nick) + .populateTransaction.checkBalance(assetToken.address); + await nick.sendTransaction(tx); + }); + it("Only Governor can approve all tokens", async () => { + const { + timelock, + strategist, + nick, + vaultSigner, + amoStrategy, + pool, + } = await context(); + + expect(await amoStrategy.connect(timelock).isGovernor()).to.equal( + true + ); + + // Timelock can approve all tokens + const tx = await amoStrategy + .connect(timelock) + .safeApproveAllTokens(); + await expect(tx).to.emit(pool, "Approval"); + + for (const signer of [strategist, nick, vaultSigner]) { + const tx = amoStrategy.connect(signer).safeApproveAllTokens(); + await expect(tx).to.be.revertedWith("Caller is not the Governor"); + } + }); + it("Only Governor can set the max depeg", async () => { + const { + timelock, + strategist, + nick, + vaultSigner, + amoStrategy, + } = await context(); + + expect(await amoStrategy.connect(timelock).isGovernor()).to.equal( + true + ); + + // Timelock can update + const newMaxDepeg = parseUnits("0.02"); + const tx = await amoStrategy + .connect(timelock) + .setMaxDepeg(newMaxDepeg); + await expect(tx) + .to.emit(amoStrategy, "MaxDepegUpdated") + .withArgs(newMaxDepeg); + + expect(await amoStrategy.maxDepeg()).to.equal(newMaxDepeg); + + for (const signer of [strategist, nick, vaultSigner]) { + const tx = amoStrategy.connect(signer).setMaxDepeg(newMaxDepeg); + await expect(tx).to.be.revertedWith("Caller is not the Governor"); + } + }); + }); + + describe("with wS in the vault", () => { + beforeEach(async () => { + const { loadFixture } = await context({ + wsMintAmount: 5000000, + depositToStrategy: false, + balancePool: true, + }); + fixture = await loadFixture(); + }); + it.only("Vault should deposit wS to AMO strategy", async function () { + await assertDeposit(parseUnits("2000")); + }); + it("Only vault can deposit wS to AMO strategy", async function () { + const { + amoStrategy, + vaultSigner, + strategist, + timelock, + nick, + assetToken, + } = await context(); + + const depositAmount = parseUnits("50"); + await assetToken + .connect(vaultSigner) + .transfer(amoStrategy.address, depositAmount); + + for (const signer of [strategist, timelock, nick]) { + const tx = amoStrategy + .connect(signer) + .deposit(assetToken.address, depositAmount); + + await expect(tx).to.revertedWith("Caller is not the Vault"); + } + }); + it("Only vault can deposit all wS to AMO strategy", async function () { + const { + amoStrategy, + pool, + vaultSigner, + strategist, + timelock, + nick, + assetToken, + } = await context(); + + const depositAmount = parseUnits("50"); + await assetToken + .connect(vaultSigner) + .transfer(amoStrategy.address, depositAmount); + + for (const signer of [strategist, timelock, nick]) { + const tx = amoStrategy.connect(signer).depositAll(); + + await expect(tx).to.revertedWith("Caller is not the Vault"); + } + + const tx = await amoStrategy.connect(vaultSigner).depositAll(); + await expect(tx) + .to.emit(amoStrategy, "Deposit") + .withNamedArgs({ _asset: assetToken.address, _pToken: pool.address }); + }); + }); + + // describe("with the strategy having OS and wS in a balanced pool", () => { + // const loadFixture = createFixtureLoader(swapXAMOFixture, { + // wsMintAmount: 100000, + // depositToStrategy: true, + // balancePool: true, + // }); + // beforeEach(async () => { + // fixture = await loadFixture(); + // }); + // it("Vault should deposit wS", async function () { + // await assertDeposit(parseUnits("5000")); + // }); + // it("Vault should be able to withdraw all", async () => { + // await assertWithdrawAll(); + // }); + // it("Vault should be able to withdraw all in SwapX Emergency", async () => { + // const { amoStrategy, swapXGauge, vaultSigner } = fixture; + + // const gaugeOwner = await swapXGauge.owner(); + // const ownerSigner = await impersonateAndFund(gaugeOwner); + // await swapXGauge.connect(ownerSigner).activateEmergencyMode(); + // await assertWithdrawAll(); + + // // Try again when the strategy is empty + // await amoStrategy.connect(vaultSigner).withdrawAll(); + // }); + // it("Should fail to deposit zero wS", async () => { + // const { amoStrategy, vaultSigner, wS } = fixture; + + // const tx = amoStrategy + // .connect(vaultSigner) + // .deposit(assetToken.address, 0); + + // await expect(tx).to.be.revertedWith("Must deposit something"); + // }); + // it("Should fail to deposit OS", async () => { + // const { amoStrategy, vaultSigner, oSonic } = fixture; + + // const tx = amoStrategy + // .connect(vaultSigner) + // .deposit(oSonic.address, parseUnits("1")); + + // await expect(tx).to.be.revertedWith("Unsupported asset"); + // }); + // it("Should fail to withdraw zero wS", async () => { + // const { amoStrategy, vaultSigner, oSonicVault, wS } = fixture; + + // const tx = amoStrategy + // .connect(vaultSigner) + // .withdraw(oSonicVault.address, assetToken.address, 0); + + // await expect(tx).to.be.revertedWith("Must withdraw something"); + // }); + // it("Should fail to withdraw OS", async () => { + // const { amoStrategy, vaultSigner, oSonic, oSonicVault } = + // fixture; + + // const tx = amoStrategy + // .connect(vaultSigner) + // .withdraw(oSonicVault.address, oSonic.address, parseUnits("1")); + + // await expect(tx).to.be.revertedWith("Unsupported asset"); + // }); + // it("Should fail to withdraw to a user", async () => { + // const { amoStrategy, vaultSigner, wS, nick } = fixture; + + // const tx = amoStrategy + // .connect(vaultSigner) + // .withdraw(nick.address, assetToken.address, parseUnits("1")); + + // await expect(tx).to.be.revertedWith("Only withdraw to vault allowed"); + // }); + // it("Vault should be able to withdraw all from empty strategy", async () => { + // const { amoStrategy, vaultSigner } = fixture; + // await assertWithdrawAll(); + + // // Now try again after all the assets have already been withdrawn + // const tx = await amoStrategy + // .connect(vaultSigner) + // .withdrawAll(); + + // // Check emitted events + // await expect(tx).to.not.emit(amoStrategy, "Withdrawal"); + // }); + // it("Vault should be able to partially withdraw", async () => { + // await assertWithdrawPartial(parseUnits("1000")); + // }); + // it("Only vault can withdraw wS from AMO strategy", async function () { + // const { amoStrategy, oSonicVault, strategist, timelock, nick, wS } = + // fixture; + + // for (const signer of [strategist, timelock, nick]) { + // const tx = amoStrategy + // .connect(signer) + // .withdraw(oSonicVault.address, assetToken.address, parseUnits("50")); + + // await expect(tx).to.revertedWith("Caller is not the Vault"); + // } + // }); + // it("Only vault and governor can withdraw all WETH from AMO strategy", async function () { + // const { amoStrategy, strategist, timelock, nick } = fixture; + + // for (const signer of [strategist, nick]) { + // const tx = amoStrategy.connect(signer).withdrawAll(); + + // await expect(tx).to.revertedWith("Caller is not the Vault or Governor"); + // } + + // // Governor can withdraw all + // const tx = amoStrategy.connect(timelock).withdrawAll(); + // await expect(tx).to.emit(amoStrategy, "Withdrawal"); + // }); + // it("Harvester can collect rewards", async () => { + // const { + // harvester, + // nick, + // amoStrategy, + // swapXGauge, + // swpx, + // strategist, + // } = fixture; + + // const swpxBalanceBefore = await swpx.balanceOf(strategist.address); + + // // Send some SWPx rewards to the gauge + // const distributorAddress = await swapXGauge.DISTRIBUTION(); + // const distributorSigner = await impersonateAndFund(distributorAddress); + // const rewardAmount = parseUnits("1000"); + // await setERC20TokenBalance(distributorAddress, swpx, rewardAmount); + // await swapXGauge + // .connect(distributorSigner) + // .notifyRewardAmount(swpx.address, rewardAmount); + + // // Harvest the rewards + // // prettier-ignore + // const tx = await harvester + // .connect(nick)["harvestAndTransfer(address)"](amoStrategy.address); + + // await expect(tx).to.emit(amoStrategy, "RewardTokenCollected"); + + // const swpxBalanceAfter = await swpx.balanceOf(strategist.address); + // log( + // `Rewards collected ${formatUnits( + // swpxBalanceAfter.sub(swpxBalanceBefore) + // )} SWPx` + // ); + // expect(swpxBalanceAfter).to.gt(swpxBalanceBefore); + // }); + // it("Attacker front-run deposit within range by adding wS to the pool", async () => { + // const { nick, oSonic, vaultSigner, amoStrategy, wS } = + // fixture; + + // const attackerWsBalanceBefore = await assetToken.balanceOf(nick.address); + // const wsAmountIn = parseUnits("20000"); + + // const dataBeforeSwap = await snapData(); + // logSnapData( + // dataBeforeSwap, + // `\nBefore attacker swaps ${formatUnits( + // wsAmountIn + // )} wS into the pool for OS` + // ); + + // // Attacker swaps a lot of wS for OS in the pool + // // This drops the pool's wS/OS price and increases the OS/wS price + // const osAmountOut = await poolSwapTokensIn(wS, wsAmountIn); + + // const depositAmount = parseUnits("200000"); + + // const dataBeforeDeposit = await snapData(); + // logSnapData( + // dataBeforeDeposit, + // `\nAfter attacker tilted pool and before strategist deposits ${formatUnits( + // depositAmount + // )} wS` + // ); + + // // Vault deposits wS to the strategy + // await wS + // .connect(vaultSigner) + // .transfer(amoStrategy.address, depositAmount); + // await amoStrategy + // .connect(vaultSigner) + // .deposit(assetToken.address, depositAmount); + + // const dataAfterDeposit = await snapData(); + // logSnapData( + // dataAfterDeposit, + // `\nAfter deposit of ${formatUnits( + // depositAmount + // )} wS to strategy and before attacker swaps ${formatUnits( + // osAmountOut + // )} OS back into the pool for wS` + // ); + // await logProfit(dataBeforeSwap); + + // // Attacker swaps the OS back for wS + // await poolSwapTokensIn(oSonic, osAmountOut); + + // const dataAfterFinalSwap = await snapData(); + // logSnapData( + // dataAfterFinalSwap, + // `\nAfter attacker swaps ${formatUnits( + // osAmountOut + // )} OS back into the pool for wS` + // ); + // await logProfit(dataBeforeSwap); + + // const attackerWsBalanceAfter = await assetToken.balanceOf(nick.address); + // log( + // `Attacker's profit ${formatUnits( + // attackerWsBalanceAfter.sub(attackerWsBalanceBefore) + // )} wS` + // ); + // }); + // describe("When attacker front-run by adding a lot of wS to the pool", () => { + // let attackerWsBalanceBefore; + // let dataBeforeSwap; + // let osAmountOut; + // beforeEach(async () => { + // const { nick, wS } = fixture; + + // attackerWsBalanceBefore = await assetToken.balanceOf(nick.address); + // const wsAmountIn = parseUnits("10000000"); + + // dataBeforeSwap = await snapData(); + // logSnapData( + // dataBeforeSwap, + // `\nBefore attacker swaps ${formatUnits( + // wsAmountIn + // )} wS into the pool for OS` + // ); + + // // Attacker swaps a lot of wS for OS in the pool + // // This drops the pool's wS/OS price and increases the OS/wS price + // osAmountOut = await poolSwapTokensIn(wS, wsAmountIn); + // }); + // it("Strategist fails to deposit to strategy", async () => { + // await assertFailedDeposit(parseUnits("5000"), "price out of range"); + // }); + // it("Strategist fails to deposit all to strategy", async () => { + // await assertFailedDepositAll(parseUnits("5000"), "price out of range"); + // }); + // it("Strategist should withdraw from strategy with a profit", async () => { + // const { + // nick, + // oSonic, + // oSonicVault, + // vaultSigner, + // amoStrategy, + // wS, + // } = fixture; + // const withdrawAmount = parseUnits("4000"); + + // const dataBeforeWithdraw = await snapData(); + // logSnapData( + // dataBeforeWithdraw, + // `\nBefore strategist withdraw ${formatUnits(withdrawAmount)} wS` + // ); + + // const tx = await amoStrategy + // .connect(vaultSigner) + // .withdraw(oSonicVault.address, assetToken.address, withdrawAmount); + + // const dataAfterWithdraw = await snapData(); + // logSnapData( + // dataAfterWithdraw, + // `\nAfter withdraw and before attacker swaps ${formatUnits( + // osAmountOut + // )} OS back into the pool for wS` + // ); + // await logProfit(dataBeforeSwap); + + // // Get how much OS was burnt + // const receipt = await tx.wait(); + // const redeemEvent = receipt.events.find( + // (e) => e.event === "Withdrawal" && e.args._asset === oSonic.address + // ); + // log(`\nWithdraw burnt ${formatUnits(redeemEvent.args._amount)} OS`); + + // // Attacker swaps the OS back for wS + // await poolSwapTokensIn(oSonic, osAmountOut); + + // const dataAfterFinalSwap = await snapData(); + // logSnapData( + // dataAfterFinalSwap, + // "\nAfter attacker swaps OS into the pool for wS" + // ); + // const profit = await logProfit(dataBeforeSwap); + // expect(profit, "vault profit").to.gt(0); + + // const attackerWsBalanceAfter = await assetToken.balanceOf(nick.address); + // log( + // `Attacker's profit/loss ${formatUnits( + // attackerWsBalanceAfter.sub(attackerWsBalanceBefore) + // )} wS` + // ); + // }); + // }); + // describe("When attacker front-run by adding a lot of OS to the pool", () => { + // const attackerBalanceBefore = {}; + // let dataBeforeSwap; + // let wsAmountOut; + // beforeEach(async () => { + // const { nick, oSonic, oSonicVault, wS } = fixture; + + // const osAmountIn = parseUnits("10000000"); + // // Mint OS using wS + // await oSonicVault.connect(nick).mint(assetToken.address, osAmountIn, 0); + + // attackerBalanceBefore.os = await oSonic.balanceOf(nick.address); + // attackerBalanceBefore.ws = await assetToken.balanceOf(nick.address); + + // dataBeforeSwap = await snapData(); + // logSnapData( + // dataBeforeSwap, + // `\nBefore attacker swaps ${formatUnits( + // osAmountIn + // )} OS into the pool for wS` + // ); + + // // Attacker swaps a lot of OS for wS in the pool + // // This increases the pool's wS/OS price and decreases the OS/wS price + // wsAmountOut = await poolSwapTokensIn(oSonic, osAmountIn); + // }); + // it("Strategist fails to deposit to strategy", async () => { + // await assertFailedDeposit(parseUnits("5000"), "price out of range"); + // }); + // it("Strategist fails to deposit all to strategy", async () => { + // await assertFailedDepositAll(parseUnits("5000"), "price out of range"); + // }); + // it("Strategist should withdraw from strategy with a profit", async () => { + // const { + // nick, + // oSonic, + // oSonicVault, + // vaultSigner, + // amoStrategy, + // wS, + // } = fixture; + // const withdrawAmount = parseUnits("200"); + + // const dataBeforeWithdraw = await snapData(); + // logSnapData( + // dataBeforeWithdraw, + // `\nBefore strategist withdraw ${formatUnits(withdrawAmount)} wS` + // ); + + // const tx = await amoStrategy + // .connect(vaultSigner) + // .withdraw(oSonicVault.address, assetToken.address, withdrawAmount); + + // const dataAfterWithdraw = await snapData(); + // logSnapData( + // dataAfterWithdraw, + // `\nAfter withdraw and before attacker swaps ${formatUnits( + // wsAmountOut + // )} wS back into the pool for OS` + // ); + // await logProfit(dataBeforeSwap); + + // // Get how much OS was burnt + // const receipt = await tx.wait(); + // const redeemEvent = receipt.events.find( + // (e) => e.event === "Withdrawal" && e.args._asset === oSonic.address + // ); + // log(`\nWithdraw burnt ${formatUnits(redeemEvent.args._amount)} OS`); + + // // Attacker swaps the wS back for OS + // await poolSwapTokensIn(wS, wsAmountOut); + + // const dataAfterFinalSwap = await snapData(); + // logSnapData( + // dataAfterFinalSwap, + // "\nAfter attacker swaps wS into the pool for OS" + // ); + // const profit = await logProfit(dataBeforeSwap); + // expect(profit, "vault profit").to.gt(0); + + // const attackerBalanceAfter = {}; + // attackerBalanceAfter.os = await oSonic.balanceOf(nick.address); + // attackerBalanceAfter.ws = await assetToken.balanceOf(nick.address); + // log( + // `Attacker's profit/loss ${formatUnits( + // attackerBalanceAfter.os.sub(attackerBalanceBefore.os) + // )} OS and ${formatUnits( + // attackerBalanceAfter.assetToken.sub(attackerBalanceBefore.ws) + // )} wS` + // ); + // }); + // }); + // }); + + // describe("with a lot more OS in the pool", () => { + // const loadFixture = createFixtureLoader(swapXAMOFixture, { + // wsMintAmount: 5000, + // depositToStrategy: true, + // balancePool: true, + // poolAddOSAmount: 1000000, + // }); + // beforeEach(async () => { + // fixture = await loadFixture(); + // }); + // it("Vault should fail to deposit wS to AMO strategy", async function () { + // await assertFailedDeposit(parseUnits("5000"), "price out of range"); + // }); + // it("Vault should be able to withdraw all", async () => { + // await assertWithdrawAll(); + // }); + // it("Vault should be able to partially withdraw", async () => { + // await assertWithdrawPartial(parseUnits("4000")); + // }); + // it("Strategist should swap a little assets to the pool", async () => { + // await assertSwapAssetsToPool(parseUnits("3")); + // }); + // it("Strategist should swap enough wS to get the pool close to balanced", async () => { + // const { pool } = fixture; + // const { _reserve0: wsReserves, _reserve1: osReserves } = + // await pool.getReserves(); + // // 5% of the extra OS + // const osAmount = osReserves.sub(wsReserves).mul(5).div(100); + // const wsAmount = osAmount.mul(wsReserves).div(osReserves); + // log(`OS amount: ${formatUnits(osAmount)}`); + // log(`wS amount: ${formatUnits(wsAmount)}`); + + // await assertSwapAssetsToPool(wsAmount); + // }); + // it("Strategist should swap a lot of assets to the pool", async () => { + // await assertSwapAssetsToPool(parseUnits("3000")); + // }); + // it("Strategist should swap most of the wS owned by the strategy", async () => { + // // TODO calculate how much wS should be swapped to get the pool balanced + // await assertSwapAssetsToPool(parseUnits("4400")); + // }); + // it("Strategist should fail to add more wS than owned by the strategy", async () => { + // const { amoStrategy, strategist } = fixture; + + // const tx = amoStrategy + // .connect(strategist) + // .swapAssetsToPool(parseUnits("2000000")); + + // await expect(tx).to.be.revertedWith("Not enough LP tokens in gauge"); + // }); + // it("Strategist should fail to add more OS to the pool", async () => { + // const { amoStrategy, strategist } = fixture; + + // // try swapping OS into the pool + // const tx = amoStrategy + // .connect(strategist) + // .swapOTokensToPool(parseUnits("0.001")); + + // await expect(tx).to.be.revertedWith("OTokens balance worse"); + // }); + // }); + + // describe("with a little more OS in the pool", () => { + // const loadFixture = createFixtureLoader(swapXAMOFixture, { + // wsMintAmount: 20000, + // depositToStrategy: true, + // balancePool: true, + // poolAddOSAmount: 5000, + // }); + // beforeEach(async () => { + // fixture = await loadFixture(); + // }); + // it("Vault should deposit wS to AMO strategy", async function () { + // await assertDeposit(parseUnits("12000")); + // }); + // it("Vault should be able to withdraw all", async () => { + // await assertWithdrawAll(); + // }); + // it("Vault should be able to partially withdraw", async () => { + // await assertWithdrawPartial(parseUnits("1000")); + // }); + // it("Strategist should swap a little assets to the pool", async () => { + // await assertSwapAssetsToPool(parseUnits("3")); + // }); + // it("Strategist should swap enough wS to get the pool close to balanced", async () => { + // const { pool } = fixture; + // const { _reserve0: wsReserves, _reserve1: osReserves } = + // await pool.getReserves(); + // // 50% of the extra OS in the pool gets close to balanced + // const osAmount = osReserves.sub(wsReserves).mul(50).div(100); + // const wsAmount = osAmount.mul(wsReserves).div(osReserves); + + // await assertSwapAssetsToPool(wsAmount); + // }); + // it("Strategist should fail to add too much wS to the pool", async () => { + // const { amoStrategy, strategist } = fixture; + + // const dataBefore = await snapData(); + // await logSnapData(dataBefore, "Before swapping assets to the pool"); + + // // try the extra OS in the pool + // const tx = amoStrategy + // .connect(strategist) + // .swapAssetsToPool(parseUnits("5000")); + + // await expect(tx).to.be.revertedWith("Assets overshot peg"); + // }); + // it("Strategist should fail to add zero wS to the pool", async () => { + // const { amoStrategy, strategist } = fixture; + + // const tx = amoStrategy.connect(strategist).swapAssetsToPool(0); + + // await expect(tx).to.be.revertedWith("Must swap something"); + // }); + // it("Strategist should fail to add more OS to the pool", async () => { + // const { amoStrategy, strategist } = fixture; + + // // try swapping OS into the pool + // const tx = amoStrategy + // .connect(strategist) + // .swapOTokensToPool(parseUnits("0.001")); + + // await expect(tx).to.be.revertedWith("OTokens balance worse"); + // }); + // }); + + // describe("with a lot more wS in the pool", () => { + // const loadFixture = createFixtureLoader(swapXAMOFixture, { + // wsMintAmount: 5000, + // depositToStrategy: true, + // balancePool: true, + // poolAddwSAmount: 2000000, + // }); + // beforeEach(async () => { + // fixture = await loadFixture(); + // }); + // it("Vault should fail to deposit wS to strategy", async function () { + // await assertFailedDeposit(parseUnits("6000"), "price out of range"); + // }); + // it("Vault should be able to withdraw all", async () => { + // await assertWithdrawAll(); + // }); + // it("Vault should be able to partially withdraw", async () => { + // await assertWithdrawPartial(parseUnits("1000")); + // }); + // it("Strategist should swap a little OS to the pool", async () => { + // const osAmount = parseUnits("0.3"); + // await assertSwapOTokensToPool(osAmount, fixture); + // }); + // it("Strategist should swap a lot of OS to the pool", async () => { + // const osAmount = parseUnits("5000"); + // await assertSwapOTokensToPool(osAmount, fixture); + // }); + // it("Strategist should get the pool close to balanced", async () => { + // const { pool } = fixture; + // const { _reserve0: wsReserves, _reserve1: osReserves } = + // await pool.getReserves(); + // // 32% of the extra wS in the pool gets pretty close to balanced + // const osAmount = wsReserves.sub(osReserves).mul(32).div(100); + + // await assertSwapOTokensToPool(osAmount); + // }); + // it("Strategist should fail to add so much OS that is overshoots", async () => { + // const { amoStrategy, strategist } = fixture; + + // // try swapping wS into the pool + // const tx = amoStrategy + // .connect(strategist) + // .swapOTokensToPool(parseUnits("999990")); + + // await expect(tx).to.be.revertedWith("OTokens overshot peg"); + // }); + // it("Strategist should fail to add more wS to the pool", async () => { + // const { amoStrategy, strategist } = fixture; + + // // try swapping wS into the pool + // const tx = amoStrategy + // .connect(strategist) + // .swapAssetsToPool(parseUnits("0.0001")); + + // await expect(tx).to.be.revertedWith("Assets balance worse"); + // }); + // }); + + // describe("with a little more wS in the pool", () => { + // const loadFixture = createFixtureLoader(swapXAMOFixture, { + // wsMintAmount: 20000, + // depositToStrategy: true, + // balancePool: true, + // poolAddwSAmount: 20000, + // }); + // beforeEach(async () => { + // fixture = await loadFixture(); + // }); + // it("Vault should deposit wS to AMO strategy", async function () { + // await assertDeposit(parseUnits("18000")); + // }); + // it("Vault should be able to withdraw all", async () => { + // await assertWithdrawAll(); + // }); + // it("Vault should be able to partially withdraw", async () => { + // await assertWithdrawPartial(parseUnits("1000")); + // }); + // it("Strategist should swap a little OS to the pool", async () => { + // const osAmount = parseUnits("8"); + // await assertSwapOTokensToPool(osAmount, fixture); + // }); + // it("Strategist should get the pool close to balanced", async () => { + // const { pool } = fixture; + + // const { _reserve0: wsReserves, _reserve1: osReserves } = + // await pool.getReserves(); + // // 50% of the extra wS in the pool gets pretty close to balanced + // const osAmount = wsReserves.sub(osReserves).mul(50).div(100); + + // await assertSwapOTokensToPool(osAmount, fixture); + // }); + // it("Strategist should fail to add zero OS to the pool", async () => { + // const { amoStrategy, strategist } = fixture; + + // const tx = amoStrategy.connect(strategist).swapOTokensToPool(0); + + // await expect(tx).to.be.revertedWith("Must swap something"); + // }); + // it("Strategist should fail to add too much OS to the pool", async () => { + // const { amoStrategy, strategist } = fixture; + + // // Add OS to the pool + // const tx = amoStrategy + // .connect(strategist) + // .swapOTokensToPool(parseUnits("11000")); + + // await expect(tx).to.be.revertedWith("OTokens overshot peg"); + // }); + // it("Strategist should fail to add more wS to the pool", async () => { + // const { amoStrategy, strategist } = fixture; + + // // try swapping wS into the pool + // const tx = amoStrategy + // .connect(strategist) + // .swapAssetsToPool(parseUnits("0.0001")); + + // await expect(tx).to.be.revertedWith("Assets balance worse"); + // }); + // }); + + // describe("with the strategy owning a small percentage of the pool", () => { + // const loadFixture = createFixtureLoader(swapXAMOFixture, { + // wsMintAmount: 5000, + // depositToStrategy: true, + // balancePool: true, + // }); + // let dataBefore; + // beforeEach(async () => { + // fixture = await loadFixture(); + + // const { nick, wS, oSonic, oSonicVault, pool } = fixture; + + // // Other users adds a lot more liquidity to the pool + // const bigAmount = parseUnits("1000000"); + // // transfer wS to the pool + // await assetToken.connect(nick).transfer(pool.address, bigAmount); + // // Mint OS with wS + // await oSonicVault.connect(nick).mint(assetToken.address, bigAmount.mul(5), 0); + // // transfer OS to the pool + // await oSonic.connect(nick).transfer(pool.address, bigAmount); + // // mint pool LP tokens + // await pool.connect(nick).mint(nick.address); + + // dataBefore = await snapData(); + // await logSnapData(dataBefore); + // }); + // it("a lot of OS is swapped into the pool", async () => { + // const { oSonic, amoStrategy, wS } = fixture; + + // // Swap OS into the pool and wS out + // await poolSwapTokensIn(oSonic, parseUnits("1005000")); + + // await logSnapData(await snapData(), "\nAfter swapping OS into the pool"); + + // // Assert the strategy's balance + // expect( + // await amoStrategy.checkBalance(assetToken.address), + // "Strategy's check balance" + // ).to.withinRange(dataBefore.stratBalance, dataBefore.stratBalance.add(1)); + + // // Swap wS into the pool and OS out + // await poolSwapTokensIn(wS, parseUnits("2000000")); + + // await logSnapData(await snapData(), "\nAfter swapping wS into the pool"); + + // // Assert the strategy's balance + // expect( + // await amoStrategy.checkBalance(assetToken.address), + // "Strategy's check balance" + // ).to.withinRange(dataBefore.stratBalance, dataBefore.stratBalance.add(1)); + // }); + // it("a lot of wS is swapped into the pool", async () => { + // const { amoStrategy, oSonic, wS } = fixture; + + // // Swap wS into the pool and OS out + // await poolSwapTokensIn(wS, parseUnits("1006000")); + + // await logSnapData(await snapData(), "\nAfter swapping wS into the pool"); + + // // Assert the strategy's balance + // expect( + // await amoStrategy.checkBalance(assetToken.address), + // "Strategy's check balance" + // ).to.withinRange(dataBefore.stratBalance, dataBefore.stratBalance.add(1)); + + // // Swap OS into the pool and wS out + // await poolSwapTokensIn(oSonic, parseUnits("1005000")); + + // await logSnapData(await snapData(), "\nAfter swapping OS into the pool"); + + // // Assert the strategy's balance + // expect( + // await amoStrategy.checkBalance(assetToken.address), + // "Strategy's check balance" + // ).to.withinRange(dataBefore.stratBalance, dataBefore.stratBalance.add(1)); + // }); + // }); + + // describe("with an insolvent vault", () => { + // const loadFixture = createFixtureLoader(swapXAMOFixture, { + // wsMintAmount: 5000000, + // depositToStrategy: false, + // }); + // beforeEach(async () => { + // fixture = await loadFixture(); + + // const { oSonicVault, vaultSigner, amoStrategy, wS } = fixture; + + // // Deposit a little to the strategy + // const littleAmount = parseUnits("100"); + // await wS + // .connect(vaultSigner) + // .transfer(amoStrategy.address, littleAmount); + // await amoStrategy + // .connect(vaultSigner) + // .deposit(assetToken.address, littleAmount); + + // const totalAssets = await oSonicVault.totalValue(); + // // Calculate a 0.21% (21 basis points) loss + // const lossAmount = totalAssets.mul(21).div(10000); + // await assetToken.connect(vaultSigner).transfer(addresses.dead, lossAmount); + // expect( + // await assetToken.balanceOf(oSonicVault.address), + // "Must have enough wS in vault to make insolvent" + // ).to.gte(lossAmount); + // }); + // it("Should fail to deposit", async () => { + // const { vaultSigner, amoStrategy, wS } = fixture; + + // // Vault calls deposit on the strategy + // const depositAmount = parseUnits("10"); + // await wS + // .connect(vaultSigner) + // .transfer(amoStrategy.address, depositAmount); + // const tx = amoStrategy + // .connect(vaultSigner) + // .deposit(assetToken.address, depositAmount); + + // await expect(tx).to.be.revertedWith("Protocol insolvent"); + // }); + // it("Should fail to withdraw", async () => { + // const { oSonicVault, vaultSigner, amoStrategy, wS } = fixture; + + // // Vault withdraws from the strategy + // const tx = amoStrategy + // .connect(vaultSigner) + // .withdraw(oSonicVault.address, assetToken.address, parseUnits("10")); + + // await expect(tx).to.be.revertedWith("Protocol insolvent"); + // }); + // it("Should withdraw all", async () => { + // const { vaultSigner, amoStrategy } = fixture; + + // // Vault withdraw alls from the strategy + // const tx = amoStrategy.connect(vaultSigner).withdrawAll(); + + // await expect(tx).to.not.revertedWith("Protocol insolvent"); + // }); + // it("Should fail to swap assets to the pool", async () => { + // const { amoStrategy, strategist } = fixture; + + // const tx = amoStrategy + // .connect(strategist) + // .swapAssetsToPool(parseUnits("10")); + + // await expect(tx).to.be.revertedWith("Protocol insolvent"); + // }); + // it("Should fail to swap OS to the pool", async () => { + // const { amoStrategy, strategist } = fixture; + + // const tx = amoStrategy + // .connect(strategist) + // .swapOTokensToPool(parseUnits("10")); + + // await expect(tx).to.be.revertedWith("Protocol insolvent"); + // }); + // }); + + const poolSwapTokensIn = async (tokenIn, amountIn) => { + const { nick, pool, wS } = fixture; + const amountOut = await pool.getAmountOut(amountIn, tokenIn.address); + await tokenIn.connect(nick).transfer(pool.address, amountIn); + if (tokenIn.address == assetToken.address) { + await pool.swap(0, amountOut, nick.address, "0x"); + } else { + await pool.swap(amountOut, 0, nick.address, "0x"); + } + + return amountOut; + }; + + const precision = parseUnits("1", 18); + // Calculate the value of asset token and OToken assuming the pool is balanced + const calcReserveValue = (reserves) => { + const k = calcInvariant(reserves); + + // If x = y, let’s denote x = y = z (where z is the common reserve value) + // Substitute z into the invariant: + // k = z^3 * z + z * z^3 + // k = 2 * z^4 + // Going back the other way to calculate the common reserve value z + // z = (k / 2) ^ (1/4) + // the total value of the pool when x = y is 2 * z, which is 2 * (k / 2) ^ (1/4) + const zSquared = sqrt(k.mul(precision).div(2)); + const z = sqrt(zSquared.mul(precision)); + return z.mul(2); + }; + + const calcInvariant = (reserves) => { + const x = reserves.assetToken; + const y = reserves.oToken; + const a = x.mul(y).div(precision); + const b = x.mul(x).div(precision).add(y.mul(y).div(precision)); + const k = a.mul(b).div(precision); + + return k; + }; + + // Babylonian square root function for Ethers.js BigNumber + function sqrt(value) { + // Convert input to BigNumber if it isn't already + let bn = ethers.BigNumber.from(value); + + // Handle edge cases + if (bn.lt(0)) { + throw new Error("Square root of negative number is not supported"); + } + if (bn.eq(0)) { + return ethers.BigNumber.from(0); + } + + // Initial guess (number / 2) + let guess = bn.div(2); + + // Define precision threshold (in wei scale, 10^-18) + const epsilon = ethers.BigNumber.from("1"); // 1 wei precision + + // Keep refining until we reach desired precision + while (true) { + // Babylonian method: nextGuess = (guess + number/guess) / 2 + // Using mul and div for BigNumber arithmetic + let numerator = guess.add(bn.div(guess)); + let nextGuess = numerator.div(2); + + // Calculate absolute difference + let diff = nextGuess.gt(guess) + ? nextGuess.sub(guess) + : guess.sub(nextGuess); + + // If difference is less than epsilon, we're done + if (diff.lte(epsilon)) { + return nextGuess; + } + + // Update guess for next iteration + guess = nextGuess; + } + } + + const snapData = async () => { + const { vault, amoStrategy, oToken, pool, gauge, assetToken } = + await context(); + + const stratBalance = await amoStrategy.checkBalance(assetToken.address); + const oTokenSupply = await oToken.totalSupply(); + const vaultAssets = await vault.totalValue(); + const poolSupply = await pool.totalSupply(); + const { assetReserves, oTokenReserves } = await getPoolReserves(); + const reserves = { assetToken: assetReserves, oToken: oTokenReserves }; + + // Amount of asset token bought from selling 1 oToken + const assetAmount = await pool.getAmountOut( + parseUnits("1"), + oToken.address + ); + // asset/oToken price = assetToken / oToken + const sellPrice = assetAmount; + + // Amount of asset token sold from buying 1 OToken + const oTokenAmount = await pool.getAmountOut(parseUnits("1"), assetToken.address); + // OToken/asset price = asset / OToken + const buyPrice = parseUnits("1", 36).div(oTokenAmount); + + const k = calcInvariant(reserves); + const stratGaugeBalance = await gauge.balanceOf( + amoStrategy.address + ); + const gaugeSupply = await gauge.totalSupply(); + const vaultAssetBalance = await assetToken.balanceOf(vault.address); + const stratAssetBalance = await assetToken.balanceOf(amoStrategy.address); + + return { + stratBalance, + oTokenSupply, + vaultAssets, + poolSupply, + reserves: { assetToken: assetReserves, oToken: oTokenReserves }, + buyPrice, + sellPrice, + stratGaugeBalance, + gaugeSupply, + vaultAssetBalance, + stratAssetBalance, + k, + }; + }; + + const logSnapData = async (data, message) => { + const totalReserves = data.reserves.assetToken.add(data.reserves.oToken); + const reserversPercentage = { + assetToken: data.reserves.assetToken.mul(10000).div(totalReserves), + oToken: data.reserves.oToken.mul(10000).div(totalReserves), + }; + const gaugePercentage = data.gaugeSupply.eq(0) + ? 0 + : data.stratGaugeBalance.mul(10000).div(data.gaugeSupply); + if (message) { + log(message); + } + log(`Strategy balance : ${formatUnits(data.stratBalance)}`); + log(`oToken supply : ${formatUnits(data.oTokenSupply)}`); + log(`Vault assets : ${formatUnits(data.vaultAssets)}`); + log(`pool supply : ${formatUnits(data.poolSupply)}`); + log( + `reserves assetToken : ${formatUnits(data.reserves.assetToken)} ${formatUnits( + reserversPercentage.assetToken, + 2 + )}%` + ); + log( + `reserves OToken : ${formatUnits(data.reserves.oToken)} ${formatUnits( + reserversPercentage.oToken, + 2 + )}%` + ); + log(`buy price : ${formatUnits(data.buyPrice)} OToken/assetToken`); + log(`sell price : ${formatUnits(data.sellPrice)} OToken/assetToken`); + log(`Invariant K : ${formatUnits(data.k)}`); + log( + `strat gauge balance : ${formatUnits( + data.stratGaugeBalance + )} ${formatUnits(gaugePercentage, 2)}%` + ); + log(`gauge supply : ${formatUnits(data.gaugeSupply)}`); + log(`vault asset balance : ${formatUnits(data.vaultAssetBalance)}`); + }; + + const logProfit = async (dataBefore) => { + const { oToken, vault, amoStrategy, assetToken } = await context(); + + const stratBalanceAfter = await amoStrategy.checkBalance(assetToken.address); + const oTokenSupplyAfter = await oToken.totalSupply(); + const vaultAssetsAfter = await vault.totalValue(); + const profit = vaultAssetsAfter + .sub(dataBefore.vaultAssets) + .add(dataBefore.oTokenSupply.sub(oTokenSupplyAfter)); + + log( + `Change strat balance: ${formatUnits( + stratBalanceAfter.sub(dataBefore.stratBalance) + )}` + ); + log( + `Change vault assets : ${formatUnits( + vaultAssetsAfter.sub(dataBefore.vaultAssets) + )}` + ); + log( + `Change oToken supply : ${formatUnits( + oTokenSupplyAfter.sub(dataBefore.oTokenSupply) + )}` + ); + log(`Profit : ${formatUnits(profit)}`); + + return profit; + }; + + const assertChangedData = async (dataBefore, delta) => { + const { oToken, vault, amoStrategy, pool, gauge, assetToken } = + await context(); + + if (delta.stratBalance != undefined) { + const expectedStratBalance = dataBefore.stratBalance.add( + delta.stratBalance + ); + log(`Expected strategy balance: ${formatUnits(expectedStratBalance)}`); + + expect(await amoStrategy.checkBalance(assetToken.address)).to.withinRange( + expectedStratBalance.sub(15), + expectedStratBalance.add(15), + "Strategy's check balance" + ); + } + + if (delta.oTokenSupply != undefined) { + const expectedSupply = dataBefore.oTokenSupply.add(delta.oTokenSupply); + expect(await oToken.totalSupply(), "oToken total supply").to.equal( + expectedSupply + ); + } + + // Check Vault's asset token balance + if (delta.vaultAssetTokenBalance != undefined) { + expect(await assetToken.balanceOf(vault.address)).to.equal( + dataBefore.vaultAssetTokenBalance.add(delta.vaultAssetTokenBalance), + "Vault's assetToken balance" + ); + } + + // Check the pool's reserves + if (delta.reserves != undefined) { + const { assetTokenReserves, oTokenReserves } = await getPoolReserves(); + + // If the asset reserves delta is a function, call it to check the asset token reserves + if (typeof delta.reserves.assetToken == "function") { + // Call test function to check the asset token reserves + delta.reserves.assetToken(assetTokenReserves); + } else { + expect(assetTokenReserves, "assetToken reserves").to.equal( + dataBefore.reserves.assetToken.add(delta.reserves.assetToken) + ); + } + // Check oToken reserves delta + expect(oTokenReserves, "oToken reserves").to.equal( + dataBefore.reserves.oToken.add(delta.reserves.oToken) + ); + } + + if (delta.stratGaugeBalance) { + // Check the strategy's gauge balance + const expectedStratGaugeBalance = dataBefore.stratGaugeBalance.add( + delta.stratGaugeBalance + ); + expect( + await gauge.balanceOf(amoStrategy.address) + ).to.withinRange( + expectedStratGaugeBalance.sub(1), + expectedStratGaugeBalance.add(1), + "Strategy's gauge balance" + ); + } + }; + + async function assertDeposit(assetDepositAmount) { + const { + nick, + amoStrategy, + oToken, + vault, + pool, + vaultSigner, + assetToken, + } = await context(); + + await vault.connect(nick).mint(assetToken.address, assetDepositAmount, 0); + + const dataBefore = await snapData(); + await logSnapData(dataBefore, "\nBefore depositing asset to strategy"); + + const { lpMintAmount, oTokenMintAmount } = await calcOTokenMintAmount( + assetDepositAmount + ); + + // Vault transfers asset to strategy + await assetToken + .connect(vaultSigner) + .transfer(amoStrategy.address, assetDepositAmount); + // Vault calls deposit on the strategy + const tx = await amoStrategy + .connect(vaultSigner) + .deposit(assetToken.address, assetDepositAmount); + + await logSnapData( + await snapData(), + `\nAfter depositing ${formatUnits(assetDepositAmount)} asset to strategy` + ); + await logProfit(dataBefore); + + console.log("HERE 1"); + // Check emitted events + await expect(tx).to.emit(amoStrategy, "Deposit") + .withArgs(assetToken.address, pool.address, assetDepositAmount); + // await expect(tx).to.emit(amoStrategy, "Deposit") + // .withArgs(oToken.address, pool.address, oTokenMintAmount); + + console.log("HERE 2"); + // Calculate the value of the asset token and oToken added to the pool if the pool was balanced + const depositValue = calcReserveValue({ + assetToken: assetDepositAmount, + oToken: oTokenMintAmount, + }); + log(`Value of deposit: ${formatUnits(depositValue)}`); + + await assertChangedData(dataBefore, { + stratBalance: depositValue, + oTokenSupply: oTokenMintAmount, + reserves: { assetToken: assetDepositAmount, oToken: oTokenMintAmount }, + vaultAssetBalance: assetDepositAmount.mul(-1), + gaugeSupply: lpMintAmount, + }); + + console.log("HERE 3"); + expect( + await pool.balanceOf(amoStrategy.address), + "Strategy's pool LP balance" + ).to.equal(0); + expect( + await oToken.balanceOf(amoStrategy.address), + "Strategy's OToken balance" + ).to.equal(0); + } + + async function assertFailedDeposit(assetDepositAmount, errorMessage) { + const { wS, vaultSigner, amoStrategy } = fixture; + + const dataBefore = await snapData(); + await logSnapData(dataBefore, "\nBefore depositing wS to strategy"); + + // Vault transfers wS to strategy + await wS + .connect(vaultSigner) + .transfer(amoStrategy.address, assetDepositAmount); + + // Vault calls deposit on the strategy + const tx = amoStrategy + .connect(vaultSigner) + .deposit(assetToken.address, assetDepositAmount); + + await expect(tx, "deposit to strategy").to.be.revertedWith(errorMessage); + } + + async function assertFailedDepositAll(assetDepositAmount, errorMessage) { + const { wS, vaultSigner, amoStrategy } = fixture; + + const dataBefore = await snapData(); + await logSnapData(dataBefore, "\nBefore depositing all wS to strategy"); + + // Vault transfers wS to strategy + await wS + .connect(vaultSigner) + .transfer(amoStrategy.address, assetDepositAmount); + + // Vault calls depositAll on the strategy + const tx = amoStrategy.connect(vaultSigner).depositAll(); + + await expect(tx, "depositAll to strategy").to.be.revertedWith(errorMessage); + } + + async function assertWithdrawAll() { + const { amoStrategy, pool, oSonic, vaultSigner, wS } = + fixture; + + const dataBefore = await snapData(); + await logSnapData(dataBefore); + + const { osBurnAmount, wsWithdrawAmount } = await calcWithdrawAllAmounts(); + + // Now try to withdraw all the wS from the strategy + const tx = await amoStrategy.connect(vaultSigner).withdrawAll(); + + await logSnapData(await snapData(), "\nAfter full withdraw"); + await logProfit(dataBefore); + + // Check emitted events + await expect(tx) + .to.emit(amoStrategy, "Withdrawal") + .withArgs(assetToken.address, pool.address, wsWithdrawAmount); + await expect(tx) + .to.emit(amoStrategy, "Withdrawal") + .withArgs(oSonic.address, pool.address, osBurnAmount); + + // Calculate the value of the wS and OS tokens removed from the pool if the pool was balanced + const withdrawValue = calcReserveValue({ + ws: wsWithdrawAmount, + os: osBurnAmount, + }); + + await assertChangedData(dataBefore, { + stratBalance: withdrawValue.mul(-1), + osSupply: osBurnAmount.mul(-1), + reserves: { ws: wsWithdrawAmount.mul(-1), os: osBurnAmount.mul(-1) }, + vaultWSBalance: wsWithdrawAmount, + stratGaugeBalance: dataBefore.stratGaugeBalance.mul(-1), + }); + + expect( + wsWithdrawAmount.add(osBurnAmount), + "wS withdraw and OS burnt >= strategy balance" + ).to.gte(dataBefore.stratBalance); + + expect( + await pool.balanceOf(amoStrategy.address), + "Strategy's pool LP balance" + ).to.equal(0); + expect( + await oSonic.balanceOf(amoStrategy.address), + "Strategy's OS balance" + ).to.equal(0); + } + + async function assertWithdrawPartial(wsWithdrawAmount) { + const { + amoStrategy, + oSonic, + pool, + oSonicVault, + vaultSigner, + wS, + } = fixture; + + const dataBefore = await snapData(); + + const { lpBurnAmount, osBurnAmount } = await calcOSWithdrawAmount( + wsWithdrawAmount + ); + + // Now try to withdraw the wS from the strategy + const tx = await amoStrategy + .connect(vaultSigner) + .withdraw(oSonicVault.address, assetToken.address, wsWithdrawAmount); + + await logSnapData( + await snapData(), + `\nAfter withdraw of ${formatUnits(wsWithdrawAmount)}` + ); + await logProfit(dataBefore); + + // Check emitted events + await expect(tx) + .to.emit(amoStrategy, "Withdrawal") + .withArgs(assetToken.address, pool.address, wsWithdrawAmount); + await expect(tx).to.emit(amoStrategy, "Withdrawal").withNamedArgs({ + _asset: oSonic.address, + _pToken: pool.address, + }); + + // Calculate the value of the wS and OS tokens removed from the pool if the pool was balanced + const withdrawValue = calcReserveValue({ + ws: wsWithdrawAmount, + os: osBurnAmount, + }); + + await assertChangedData(dataBefore, { + stratBalance: withdrawValue.mul(-1), + osSupply: osBurnAmount.mul(-1), + reserves: { + ws: (actualWsReserve) => { + const expectedWsReserves = + dataBefore.reserves.assetToken.sub(wsWithdrawAmount); + + expect(actualWsReserve).to.withinRange( + expectedWsReserves.sub(50), + expectedWsReserves, + "wS reserves" + ); + }, + os: osBurnAmount.mul(-1), + }, + vaultWSBalance: wsWithdrawAmount, + gaugeSupply: lpBurnAmount.mul(-1), + }); + + expect( + await pool.balanceOf(amoStrategy.address), + "Strategy's pool LP balance" + ).to.equal(0); + expect( + await oSonic.balanceOf(amoStrategy.address), + "Strategy's OS balance" + ).to.equal(0); + } + + async function assertSwapAssetsToPool(wsAmount) { + const { oSonic, amoStrategy, pool, strategist, wS } = fixture; + + const dataBefore = await snapData(); + await logSnapData( + dataBefore, + `Before swapping ${formatUnits(wsAmount)} wS into the pool` + ); + + const { lpBurnAmount: expectedLpBurnAmount, osBurnAmount: osBurnAmount1 } = + await calcOSWithdrawAmount(wsAmount); + // TODO this is not accurate as the liquidity needs to be removed first + const osBurnAmount2 = await pool.getAmountOut(wsAmount, assetToken.address); + const osBurnAmount = osBurnAmount1.add(osBurnAmount2); + + // Swap wS to the pool and burn the received OS from the pool + const tx = await amoStrategy + .connect(strategist) + .swapAssetsToPool(wsAmount); + + await logSnapData(await snapData(), "\nAfter swapping assets to the pool"); + await logProfit(dataBefore); + + // Check emitted event + await expect(tx).to.emittedEvent("SwapAssetsToPool", [ + (actualWsAmount) => { + expect(actualWsAmount).to.withinRange( + wsAmount.sub(1), + wsAmount.add(1), + "SwapAssetsToPool event wsAmount" + ); + }, + expectedLpBurnAmount, + (actualOsBurnAmount) => { + // TODO this can be tightened once osBurnAmount is more accurately calculated + expect(actualOsBurnAmount).to.approxEqualTolerance( + osBurnAmount, + 10, + "SwapAssetsToPool event osBurnt" + ); + }, + ]); + + await assertChangedData( + dataBefore, + { + // stratBalance: osBurnAmount.mul(-1), + vaultWSBalance: 0, + stratGaugeBalance: 0, + }, + fixture + ); + + expect( + await pool.balanceOf(amoStrategy.address), + "Strategy's pool LP balance" + ).to.equal(0); + expect( + await oSonic.balanceOf(amoStrategy.address), + "Strategy's OS balance" + ).to.equal(0); + } + + async function assertSwapOTokensToPool(osAmount) { + const { oSonic, pool, amoStrategy, strategist } = fixture; + + const dataBefore = await snapData(); + await logSnapData(dataBefore, "Before swapping OTokens to the pool"); + + // Mint OS and swap into the pool, then mint more OS to add with the wS swapped out + const tx = await amoStrategy + .connect(strategist) + .swapOTokensToPool(osAmount); + + // Check emitted event + await expect(tx) + .emit(amoStrategy, "SwapOTokensToPool") + .withNamedArgs({ oTokenMinted: osAmount }); + + await logSnapData(await snapData(), "\nAfter swapping OTokens to the pool"); + await logProfit(dataBefore); + + await assertChangedData( + dataBefore, + { + // stratBalance: osBurnAmount.mul(-1), + vaultWSBalance: 0, + stratGaugeBalance: 0, + }, + fixture + ); + + expect( + await pool.balanceOf(amoStrategy.address), + "Strategy's pool LP balance" + ).to.equal(0); + expect( + await oSonic.balanceOf(amoStrategy.address), + "Strategy's OS balance" + ).to.equal(0); + } + + // Calculate the minted OS amount for a deposit + async function calcOTokenMintAmount(assetDepositAmount) { + const { pool } = await context(); + + const { assetReserves, oTokenReserves } = await getPoolReserves(); + + const oTokenMintAmount = assetDepositAmount.mul(oTokenReserves).div(assetReserves); + log(`OToken mint amount : ${formatUnits(oTokenMintAmount)}`); + + const lpTotalSupply = await pool.totalSupply(); + const lpMintAmount = assetDepositAmount.mul(lpTotalSupply).div(assetReserves); + + return { lpMintAmount, oTokenMintAmount }; + } + + async function getPoolReserves() { + const { pool, oTokenPoolIndex } = await context(); + + let assetReserves, oTokenReserves; + // Get the reserves of the pool + const { _reserve0, _reserve1 } = await pool.getReserves(); + + assetReserves = oTokenPoolIndex === 0 ? _reserve1 : _reserve0; + oTokenReserves = oTokenPoolIndex === 0 ? _reserve0 : _reserve1; + + return { assetReserves, oTokenReserves }; + } + // Calculate the amount of OS burnt from a withdraw + async function calcOSWithdrawAmount(wsWithdrawAmount) { + const { pool } = fixture; + + // Get the reserves of the pool + const { _reserve0: wsReserves, _reserve1: osReserves } = + await pool.getReserves(); + + // lp tokens to burn = wS withdrawn * total LP supply / wS pool balance + const totalLpSupply = await pool.totalSupply(); + const lpBurnAmount = wsWithdrawAmount + .mul(totalLpSupply) + .div(wsReserves) + .add(1); + // OS to burn = LP tokens to burn * OS reserves / total LP supply + const osBurnAmount = lpBurnAmount.mul(osReserves).div(totalLpSupply); + + log(`OS burn amount : ${formatUnits(osBurnAmount)}`); + + return { lpBurnAmount, osBurnAmount }; + } + + // Calculate the OS and wS amounts from a withdrawAll + async function calcWithdrawAllAmounts() { + const { amoStrategy, swapXGauge, pool } = fixture; + + // Get the reserves of the pool + const { _reserve0: wsReserves, _reserve1: osReserves } = + await pool.getReserves(); + const strategyLpAmount = await swapXGauge.balanceOf( + amoStrategy.address + ); + const totalLpSupply = await pool.totalSupply(); + + // wS to withdraw = wS pool balance * strategy LP amount / total pool LP amount + const wsWithdrawAmount = wsReserves + .mul(strategyLpAmount) + .div(totalLpSupply); + // OS to burn = OS pool balance * strategy LP amount / total pool LP amount + const osBurnAmount = osReserves.mul(strategyLpAmount).div(totalLpSupply); + + log(`wS withdraw amount : ${formatUnits(wsWithdrawAmount)}`); + log(`OS burn amount : ${formatUnits(osBurnAmount)}`); + + return { + wsWithdrawAmount, + osBurnAmount, + }; + } + }); +}; + +module.exports = { + shouldBehaveLikeAlgebraAmoStrategy, +}; \ No newline at end of file diff --git a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js index 65c7c4df22..000ca84901 100644 --- a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js +++ b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js @@ -1,1636 +1,29 @@ -const { expect } = require("chai"); -const { formatUnits, parseUnits } = require("ethers/lib/utils"); - -const { createFixtureLoader } = require("../../_fixture"); const { swapXAMOFixture } = require("../../_fixture-sonic"); -const { isCI } = require("../../helpers"); const addresses = require("../../../utils/addresses"); -const { impersonateAndFund } = require("../../../utils/signers"); -const { setERC20TokenBalance } = require("../../_fund"); - -const log = require("../../../utils/logger")("test:fork:sonic:swapx:amo"); - -describe("Sonic ForkTest: SwapX AMO Strategy", function () { - // Retry up to 3 times on CI - this.retries(isCI ? 3 : 0); - - let fixture; - - describe("post deployment", () => { - const loadFixture = createFixtureLoader(swapXAMOFixture); - beforeEach(async () => { - fixture = await loadFixture(); - }); - it("Should have constants and immutables set", async () => { - const { swapXAMOStrategy } = fixture; - - expect(await swapXAMOStrategy.SOLVENCY_THRESHOLD()).to.equal( - parseUnits("0.998", 18) - ); - expect(await swapXAMOStrategy.asset()).to.equal(addresses.sonic.wS); - expect(await swapXAMOStrategy.oToken()).to.equal( - addresses.sonic.OSonicProxy - ); - expect(await swapXAMOStrategy.pool()).to.equal( - addresses.sonic.SwapXWSOS.pool - ); - expect(await swapXAMOStrategy.gauge()).to.equal( - addresses.sonic.SwapXWSOS.gauge - ); - expect(await swapXAMOStrategy.governor()).to.equal( - addresses.sonic.timelock - ); - expect(await swapXAMOStrategy.supportsAsset(addresses.sonic.wS)).to.true; - expect(await swapXAMOStrategy.maxDepeg()).to.equal(parseUnits("0.01")); - }); - it("Should be able to check balance", async () => { - const { wS, nick, swapXAMOStrategy } = fixture; - - const balance = await swapXAMOStrategy.checkBalance(wS.address); - log(`check balance ${balance}`); - expect(balance).gte(0); - - // This uses a transaction to call a view function so the gas usage can be reported. - const tx = await swapXAMOStrategy - .connect(nick) - .populateTransaction.checkBalance(wS.address); - await nick.sendTransaction(tx); - }); - it("Only Governor can approve all tokens", async () => { - const { - timelock, - strategist, - nick, - oSonicVaultSigner, - swapXAMOStrategy, - swapXPool, - } = fixture; - - expect(await swapXAMOStrategy.connect(timelock).isGovernor()).to.equal( - true - ); - - // Timelock can approve all tokens - const tx = await swapXAMOStrategy - .connect(timelock) - .safeApproveAllTokens(); - await expect(tx).to.emit(swapXPool, "Approval"); - - for (const signer of [strategist, nick, oSonicVaultSigner]) { - const tx = swapXAMOStrategy.connect(signer).safeApproveAllTokens(); - await expect(tx).to.be.revertedWith("Caller is not the Governor"); - } - }); - it("Only Governor can set the max depeg", async () => { - const { - timelock, - strategist, - nick, - oSonicVaultSigner, - swapXAMOStrategy, - } = fixture; - - expect(await swapXAMOStrategy.connect(timelock).isGovernor()).to.equal( - true - ); - - // Timelock can update - const newMaxDepeg = parseUnits("0.02"); - const tx = await swapXAMOStrategy - .connect(timelock) - .setMaxDepeg(newMaxDepeg); - await expect(tx) - .to.emit(swapXAMOStrategy, "MaxDepegUpdated") - .withArgs(newMaxDepeg); - - expect(await swapXAMOStrategy.maxDepeg()).to.equal(newMaxDepeg); - - for (const signer of [strategist, nick, oSonicVaultSigner]) { - const tx = swapXAMOStrategy.connect(signer).setMaxDepeg(newMaxDepeg); - await expect(tx).to.be.revertedWith("Caller is not the Governor"); - } - }); - }); - - describe("with wS in the vault", () => { - const loadFixture = createFixtureLoader(swapXAMOFixture, { - wsMintAmount: 5000000, - depositToStrategy: false, - balancePool: true, - }); - beforeEach(async () => { - fixture = await loadFixture(); - }); - it("Vault should deposit wS to AMO strategy", async function () { - await assertDeposit(parseUnits("2000")); - }); - it("Only vault can deposit wS to AMO strategy", async function () { - const { - swapXAMOStrategy, - oSonicVaultSigner, - strategist, - timelock, - nick, - wS, - } = fixture; - - const depositAmount = parseUnits("50"); - await wS - .connect(oSonicVaultSigner) - .transfer(swapXAMOStrategy.address, depositAmount); - - for (const signer of [strategist, timelock, nick]) { - const tx = swapXAMOStrategy - .connect(signer) - .deposit(wS.address, depositAmount); - - await expect(tx).to.revertedWith("Caller is not the Vault"); - } - }); - it("Only vault can deposit all wS to AMO strategy", async function () { - const { - swapXAMOStrategy, - swapXPool, - oSonicVaultSigner, - strategist, - timelock, - nick, - wS, - } = fixture; - - const depositAmount = parseUnits("50"); - await wS - .connect(oSonicVaultSigner) - .transfer(swapXAMOStrategy.address, depositAmount); - - for (const signer of [strategist, timelock, nick]) { - const tx = swapXAMOStrategy.connect(signer).depositAll(); - - await expect(tx).to.revertedWith("Caller is not the Vault"); - } - - const tx = await swapXAMOStrategy.connect(oSonicVaultSigner).depositAll(); - await expect(tx) - .to.emit(swapXAMOStrategy, "Deposit") - .withNamedArgs({ _asset: wS.address, _pToken: swapXPool.address }); - }); - }); - - describe("with the strategy having OS and wS in a balanced pool", () => { - const loadFixture = createFixtureLoader(swapXAMOFixture, { - wsMintAmount: 100000, - depositToStrategy: true, - balancePool: true, - }); - beforeEach(async () => { - fixture = await loadFixture(); - }); - it("Vault should deposit wS", async function () { - await assertDeposit(parseUnits("5000")); - }); - it("Vault should be able to withdraw all", async () => { - await assertWithdrawAll(); - }); - it("Vault should be able to withdraw all in SwapX Emergency", async () => { - const { swapXAMOStrategy, swapXGauge, oSonicVaultSigner } = fixture; - - const gaugeOwner = await swapXGauge.owner(); - const ownerSigner = await impersonateAndFund(gaugeOwner); - await swapXGauge.connect(ownerSigner).activateEmergencyMode(); - await assertWithdrawAll(); - - // Try again when the strategy is empty - await swapXAMOStrategy.connect(oSonicVaultSigner).withdrawAll(); - }); - it("Should fail to deposit zero wS", async () => { - const { swapXAMOStrategy, oSonicVaultSigner, wS } = fixture; - - const tx = swapXAMOStrategy - .connect(oSonicVaultSigner) - .deposit(wS.address, 0); - - await expect(tx).to.be.revertedWith("Must deposit something"); - }); - it("Should fail to deposit OS", async () => { - const { swapXAMOStrategy, oSonicVaultSigner, oSonic } = fixture; - - const tx = swapXAMOStrategy - .connect(oSonicVaultSigner) - .deposit(oSonic.address, parseUnits("1")); - - await expect(tx).to.be.revertedWith("Unsupported asset"); - }); - it("Should fail to withdraw zero wS", async () => { - const { swapXAMOStrategy, oSonicVaultSigner, oSonicVault, wS } = fixture; - - const tx = swapXAMOStrategy - .connect(oSonicVaultSigner) - .withdraw(oSonicVault.address, wS.address, 0); - - await expect(tx).to.be.revertedWith("Must withdraw something"); - }); - it("Should fail to withdraw OS", async () => { - const { swapXAMOStrategy, oSonicVaultSigner, oSonic, oSonicVault } = - fixture; - - const tx = swapXAMOStrategy - .connect(oSonicVaultSigner) - .withdraw(oSonicVault.address, oSonic.address, parseUnits("1")); - - await expect(tx).to.be.revertedWith("Unsupported asset"); - }); - it("Should fail to withdraw to a user", async () => { - const { swapXAMOStrategy, oSonicVaultSigner, wS, nick } = fixture; - - const tx = swapXAMOStrategy - .connect(oSonicVaultSigner) - .withdraw(nick.address, wS.address, parseUnits("1")); - - await expect(tx).to.be.revertedWith("Only withdraw to vault allowed"); - }); - it("Vault should be able to withdraw all from empty strategy", async () => { - const { swapXAMOStrategy, oSonicVaultSigner } = fixture; - await assertWithdrawAll(); - - // Now try again after all the assets have already been withdrawn - const tx = await swapXAMOStrategy - .connect(oSonicVaultSigner) - .withdrawAll(); - - // Check emitted events - await expect(tx).to.not.emit(swapXAMOStrategy, "Withdrawal"); - }); - it("Vault should be able to partially withdraw", async () => { - await assertWithdrawPartial(parseUnits("1000")); - }); - it("Only vault can withdraw wS from AMO strategy", async function () { - const { swapXAMOStrategy, oSonicVault, strategist, timelock, nick, wS } = - fixture; - - for (const signer of [strategist, timelock, nick]) { - const tx = swapXAMOStrategy - .connect(signer) - .withdraw(oSonicVault.address, wS.address, parseUnits("50")); - - await expect(tx).to.revertedWith("Caller is not the Vault"); - } - }); - it("Only vault and governor can withdraw all WETH from AMO strategy", async function () { - const { swapXAMOStrategy, strategist, timelock, nick } = fixture; - - for (const signer of [strategist, nick]) { - const tx = swapXAMOStrategy.connect(signer).withdrawAll(); - - await expect(tx).to.revertedWith("Caller is not the Vault or Governor"); - } - - // Governor can withdraw all - const tx = swapXAMOStrategy.connect(timelock).withdrawAll(); - await expect(tx).to.emit(swapXAMOStrategy, "Withdrawal"); - }); - it("Harvester can collect rewards", async () => { - const { - harvester, - nick, - swapXAMOStrategy, - swapXGauge, - swpx, - strategist, - } = fixture; - - const swpxBalanceBefore = await swpx.balanceOf(strategist.address); - - // Send some SWPx rewards to the gauge - const distributorAddress = await swapXGauge.DISTRIBUTION(); - const distributorSigner = await impersonateAndFund(distributorAddress); - const rewardAmount = parseUnits("1000"); - await setERC20TokenBalance(distributorAddress, swpx, rewardAmount); - await swapXGauge - .connect(distributorSigner) - .notifyRewardAmount(swpx.address, rewardAmount); - - // Harvest the rewards - // prettier-ignore - const tx = await harvester - .connect(nick)["harvestAndTransfer(address)"](swapXAMOStrategy.address); - - await expect(tx).to.emit(swapXAMOStrategy, "RewardTokenCollected"); - - const swpxBalanceAfter = await swpx.balanceOf(strategist.address); - log( - `Rewards collected ${formatUnits( - swpxBalanceAfter.sub(swpxBalanceBefore) - )} SWPx` - ); - expect(swpxBalanceAfter).to.gt(swpxBalanceBefore); - }); - it("Attacker front-run deposit within range by adding wS to the pool", async () => { - const { clement, oSonic, oSonicVaultSigner, swapXAMOStrategy, wS } = - fixture; - - const attackerWsBalanceBefore = await wS.balanceOf(clement.address); - const wsAmountIn = parseUnits("20000"); - - const dataBeforeSwap = await snapData(); - logSnapData( - dataBeforeSwap, - `\nBefore attacker swaps ${formatUnits( - wsAmountIn - )} wS into the pool for OS` - ); - - // Attacker swaps a lot of wS for OS in the pool - // This drops the pool's wS/OS price and increases the OS/wS price - const osAmountOut = await poolSwapTokensIn(wS, wsAmountIn); - - const depositAmount = parseUnits("200000"); - - const dataBeforeDeposit = await snapData(); - logSnapData( - dataBeforeDeposit, - `\nAfter attacker tilted pool and before strategist deposits ${formatUnits( - depositAmount - )} wS` - ); - - // Vault deposits wS to the strategy - await wS - .connect(oSonicVaultSigner) - .transfer(swapXAMOStrategy.address, depositAmount); - await swapXAMOStrategy - .connect(oSonicVaultSigner) - .deposit(wS.address, depositAmount); - - const dataAfterDeposit = await snapData(); - logSnapData( - dataAfterDeposit, - `\nAfter deposit of ${formatUnits( - depositAmount - )} wS to strategy and before attacker swaps ${formatUnits( - osAmountOut - )} OS back into the pool for wS` - ); - await logProfit(dataBeforeSwap); - - // Attacker swaps the OS back for wS - await poolSwapTokensIn(oSonic, osAmountOut); - - const dataAfterFinalSwap = await snapData(); - logSnapData( - dataAfterFinalSwap, - `\nAfter attacker swaps ${formatUnits( - osAmountOut - )} OS back into the pool for wS` - ); - await logProfit(dataBeforeSwap); - - const attackerWsBalanceAfter = await wS.balanceOf(clement.address); - log( - `Attacker's profit ${formatUnits( - attackerWsBalanceAfter.sub(attackerWsBalanceBefore) - )} wS` - ); - }); - describe("When attacker front-run by adding a lot of wS to the pool", () => { - let attackerWsBalanceBefore; - let dataBeforeSwap; - let osAmountOut; - beforeEach(async () => { - const { clement, wS } = fixture; - - attackerWsBalanceBefore = await wS.balanceOf(clement.address); - const wsAmountIn = parseUnits("10000000"); - - dataBeforeSwap = await snapData(); - logSnapData( - dataBeforeSwap, - `\nBefore attacker swaps ${formatUnits( - wsAmountIn - )} wS into the pool for OS` - ); - - // Attacker swaps a lot of wS for OS in the pool - // This drops the pool's wS/OS price and increases the OS/wS price - osAmountOut = await poolSwapTokensIn(wS, wsAmountIn); - }); - it("Strategist fails to deposit to strategy", async () => { - await assertFailedDeposit(parseUnits("5000"), "price out of range"); - }); - it("Strategist fails to deposit all to strategy", async () => { - await assertFailedDepositAll(parseUnits("5000"), "price out of range"); - }); - it("Strategist should withdraw from strategy with a profit", async () => { - const { - clement, - oSonic, - oSonicVault, - oSonicVaultSigner, - swapXAMOStrategy, - wS, - } = fixture; - const withdrawAmount = parseUnits("4000"); - - const dataBeforeWithdraw = await snapData(); - logSnapData( - dataBeforeWithdraw, - `\nBefore strategist withdraw ${formatUnits(withdrawAmount)} wS` - ); - - const tx = await swapXAMOStrategy - .connect(oSonicVaultSigner) - .withdraw(oSonicVault.address, wS.address, withdrawAmount); - - const dataAfterWithdraw = await snapData(); - logSnapData( - dataAfterWithdraw, - `\nAfter withdraw and before attacker swaps ${formatUnits( - osAmountOut - )} OS back into the pool for wS` - ); - await logProfit(dataBeforeSwap); - - // Get how much OS was burnt - const receipt = await tx.wait(); - const redeemEvent = receipt.events.find( - (e) => e.event === "Withdrawal" && e.args._asset === oSonic.address - ); - log(`\nWithdraw burnt ${formatUnits(redeemEvent.args._amount)} OS`); - - // Attacker swaps the OS back for wS - await poolSwapTokensIn(oSonic, osAmountOut); - - const dataAfterFinalSwap = await snapData(); - logSnapData( - dataAfterFinalSwap, - "\nAfter attacker swaps OS into the pool for wS" - ); - const profit = await logProfit(dataBeforeSwap); - expect(profit, "vault profit").to.gt(0); - - const attackerWsBalanceAfter = await wS.balanceOf(clement.address); - log( - `Attacker's profit/loss ${formatUnits( - attackerWsBalanceAfter.sub(attackerWsBalanceBefore) - )} wS` - ); - }); - }); - describe("When attacker front-run by adding a lot of OS to the pool", () => { - const attackerBalanceBefore = {}; - let dataBeforeSwap; - let wsAmountOut; - beforeEach(async () => { - const { clement, oSonic, oSonicVault, wS } = fixture; - - const osAmountIn = parseUnits("10000000"); - // Mint OS using wS - await oSonicVault.connect(clement).mint(wS.address, osAmountIn, 0); - - attackerBalanceBefore.os = await oSonic.balanceOf(clement.address); - attackerBalanceBefore.ws = await wS.balanceOf(clement.address); - - dataBeforeSwap = await snapData(); - logSnapData( - dataBeforeSwap, - `\nBefore attacker swaps ${formatUnits( - osAmountIn - )} OS into the pool for wS` - ); - - // Attacker swaps a lot of OS for wS in the pool - // This increases the pool's wS/OS price and decreases the OS/wS price - wsAmountOut = await poolSwapTokensIn(oSonic, osAmountIn); - }); - it("Strategist fails to deposit to strategy", async () => { - await assertFailedDeposit(parseUnits("5000"), "price out of range"); - }); - it("Strategist fails to deposit all to strategy", async () => { - await assertFailedDepositAll(parseUnits("5000"), "price out of range"); - }); - it("Strategist should withdraw from strategy with a profit", async () => { - const { - clement, - oSonic, - oSonicVault, - oSonicVaultSigner, - swapXAMOStrategy, - wS, - } = fixture; - const withdrawAmount = parseUnits("200"); - - const dataBeforeWithdraw = await snapData(); - logSnapData( - dataBeforeWithdraw, - `\nBefore strategist withdraw ${formatUnits(withdrawAmount)} wS` - ); - - const tx = await swapXAMOStrategy - .connect(oSonicVaultSigner) - .withdraw(oSonicVault.address, wS.address, withdrawAmount); - - const dataAfterWithdraw = await snapData(); - logSnapData( - dataAfterWithdraw, - `\nAfter withdraw and before attacker swaps ${formatUnits( - wsAmountOut - )} wS back into the pool for OS` - ); - await logProfit(dataBeforeSwap); - - // Get how much OS was burnt - const receipt = await tx.wait(); - const redeemEvent = receipt.events.find( - (e) => e.event === "Withdrawal" && e.args._asset === oSonic.address - ); - log(`\nWithdraw burnt ${formatUnits(redeemEvent.args._amount)} OS`); - - // Attacker swaps the wS back for OS - await poolSwapTokensIn(wS, wsAmountOut); - - const dataAfterFinalSwap = await snapData(); - logSnapData( - dataAfterFinalSwap, - "\nAfter attacker swaps wS into the pool for OS" - ); - const profit = await logProfit(dataBeforeSwap); - expect(profit, "vault profit").to.gt(0); - - const attackerBalanceAfter = {}; - attackerBalanceAfter.os = await oSonic.balanceOf(clement.address); - attackerBalanceAfter.ws = await wS.balanceOf(clement.address); - log( - `Attacker's profit/loss ${formatUnits( - attackerBalanceAfter.os.sub(attackerBalanceBefore.os) - )} OS and ${formatUnits( - attackerBalanceAfter.ws.sub(attackerBalanceBefore.ws) - )} wS` - ); - }); - }); - }); - - describe("with a lot more OS in the pool", () => { - const loadFixture = createFixtureLoader(swapXAMOFixture, { - wsMintAmount: 5000, - depositToStrategy: true, - balancePool: true, - poolAddOSAmount: 1000000, - }); - beforeEach(async () => { - fixture = await loadFixture(); - }); - it("Vault should fail to deposit wS to AMO strategy", async function () { - await assertFailedDeposit(parseUnits("5000"), "price out of range"); - }); - it("Vault should be able to withdraw all", async () => { - await assertWithdrawAll(); - }); - it("Vault should be able to partially withdraw", async () => { - await assertWithdrawPartial(parseUnits("4000")); - }); - it("Strategist should swap a little assets to the pool", async () => { - await assertSwapAssetsToPool(parseUnits("3")); - }); - it("Strategist should swap enough wS to get the pool close to balanced", async () => { - const { swapXPool } = fixture; - const { _reserve0: wsReserves, _reserve1: osReserves } = - await swapXPool.getReserves(); - // 5% of the extra OS - const osAmount = osReserves.sub(wsReserves).mul(5).div(100); - const wsAmount = osAmount.mul(wsReserves).div(osReserves); - log(`OS amount: ${formatUnits(osAmount)}`); - log(`wS amount: ${formatUnits(wsAmount)}`); - - await assertSwapAssetsToPool(wsAmount); - }); - it("Strategist should swap a lot of assets to the pool", async () => { - await assertSwapAssetsToPool(parseUnits("3000")); - }); - it("Strategist should swap most of the wS owned by the strategy", async () => { - // TODO calculate how much wS should be swapped to get the pool balanced - await assertSwapAssetsToPool(parseUnits("4400")); - }); - it("Strategist should fail to add more wS than owned by the strategy", async () => { - const { swapXAMOStrategy, strategist } = fixture; - - const tx = swapXAMOStrategy - .connect(strategist) - .swapAssetsToPool(parseUnits("2000000")); - - await expect(tx).to.be.revertedWith("Not enough LP tokens in gauge"); - }); - it("Strategist should fail to add more OS to the pool", async () => { - const { swapXAMOStrategy, strategist } = fixture; - - // try swapping OS into the pool - const tx = swapXAMOStrategy - .connect(strategist) - .swapOTokensToPool(parseUnits("0.001")); - - await expect(tx).to.be.revertedWith("OTokens balance worse"); - }); - }); - - describe("with a little more OS in the pool", () => { - const loadFixture = createFixtureLoader(swapXAMOFixture, { - wsMintAmount: 20000, - depositToStrategy: true, - balancePool: true, - poolAddOSAmount: 5000, - }); - beforeEach(async () => { - fixture = await loadFixture(); - }); - it("Vault should deposit wS to AMO strategy", async function () { - await assertDeposit(parseUnits("12000")); - }); - it("Vault should be able to withdraw all", async () => { - await assertWithdrawAll(); - }); - it("Vault should be able to partially withdraw", async () => { - await assertWithdrawPartial(parseUnits("1000")); - }); - it("Strategist should swap a little assets to the pool", async () => { - await assertSwapAssetsToPool(parseUnits("3")); - }); - it("Strategist should swap enough wS to get the pool close to balanced", async () => { - const { swapXPool } = fixture; - const { _reserve0: wsReserves, _reserve1: osReserves } = - await swapXPool.getReserves(); - // 50% of the extra OS in the pool gets close to balanced - const osAmount = osReserves.sub(wsReserves).mul(50).div(100); - const wsAmount = osAmount.mul(wsReserves).div(osReserves); - - await assertSwapAssetsToPool(wsAmount); - }); - it("Strategist should fail to add too much wS to the pool", async () => { - const { swapXAMOStrategy, strategist } = fixture; - - const dataBefore = await snapData(); - await logSnapData(dataBefore, "Before swapping assets to the pool"); - - // try the extra OS in the pool - const tx = swapXAMOStrategy - .connect(strategist) - .swapAssetsToPool(parseUnits("5000")); - - await expect(tx).to.be.revertedWith("Assets overshot peg"); - }); - it("Strategist should fail to add zero wS to the pool", async () => { - const { swapXAMOStrategy, strategist } = fixture; - - const tx = swapXAMOStrategy.connect(strategist).swapAssetsToPool(0); - - await expect(tx).to.be.revertedWith("Must swap something"); - }); - it("Strategist should fail to add more OS to the pool", async () => { - const { swapXAMOStrategy, strategist } = fixture; - - // try swapping OS into the pool - const tx = swapXAMOStrategy - .connect(strategist) - .swapOTokensToPool(parseUnits("0.001")); - - await expect(tx).to.be.revertedWith("OTokens balance worse"); - }); - }); - - describe("with a lot more wS in the pool", () => { - const loadFixture = createFixtureLoader(swapXAMOFixture, { - wsMintAmount: 5000, - depositToStrategy: true, - balancePool: true, - poolAddwSAmount: 2000000, - }); - beforeEach(async () => { - fixture = await loadFixture(); - }); - it("Vault should fail to deposit wS to strategy", async function () { - await assertFailedDeposit(parseUnits("6000"), "price out of range"); - }); - it("Vault should be able to withdraw all", async () => { - await assertWithdrawAll(); - }); - it("Vault should be able to partially withdraw", async () => { - await assertWithdrawPartial(parseUnits("1000")); - }); - it("Strategist should swap a little OS to the pool", async () => { - const osAmount = parseUnits("0.3"); - await assertSwapOTokensToPool(osAmount, fixture); - }); - it("Strategist should swap a lot of OS to the pool", async () => { - const osAmount = parseUnits("5000"); - await assertSwapOTokensToPool(osAmount, fixture); - }); - it("Strategist should get the pool close to balanced", async () => { - const { swapXPool } = fixture; - const { _reserve0: wsReserves, _reserve1: osReserves } = - await swapXPool.getReserves(); - // 32% of the extra wS in the pool gets pretty close to balanced - const osAmount = wsReserves.sub(osReserves).mul(32).div(100); - - await assertSwapOTokensToPool(osAmount); - }); - it("Strategist should fail to add so much OS that is overshoots", async () => { - const { swapXAMOStrategy, strategist } = fixture; - - // try swapping wS into the pool - const tx = swapXAMOStrategy - .connect(strategist) - .swapOTokensToPool(parseUnits("999990")); - - await expect(tx).to.be.revertedWith("OTokens overshot peg"); - }); - it("Strategist should fail to add more wS to the pool", async () => { - const { swapXAMOStrategy, strategist } = fixture; - - // try swapping wS into the pool - const tx = swapXAMOStrategy - .connect(strategist) - .swapAssetsToPool(parseUnits("0.0001")); - - await expect(tx).to.be.revertedWith("Assets balance worse"); - }); - }); - - describe("with a little more wS in the pool", () => { - const loadFixture = createFixtureLoader(swapXAMOFixture, { - wsMintAmount: 20000, - depositToStrategy: true, - balancePool: true, - poolAddwSAmount: 20000, - }); - beforeEach(async () => { - fixture = await loadFixture(); - }); - it("Vault should deposit wS to AMO strategy", async function () { - await assertDeposit(parseUnits("18000")); - }); - it("Vault should be able to withdraw all", async () => { - await assertWithdrawAll(); - }); - it("Vault should be able to partially withdraw", async () => { - await assertWithdrawPartial(parseUnits("1000")); - }); - it("Strategist should swap a little OS to the pool", async () => { - const osAmount = parseUnits("8"); - await assertSwapOTokensToPool(osAmount, fixture); - }); - it("Strategist should get the pool close to balanced", async () => { - const { swapXPool } = fixture; - - const { _reserve0: wsReserves, _reserve1: osReserves } = - await swapXPool.getReserves(); - // 50% of the extra wS in the pool gets pretty close to balanced - const osAmount = wsReserves.sub(osReserves).mul(50).div(100); - - await assertSwapOTokensToPool(osAmount, fixture); - }); - it("Strategist should fail to add zero OS to the pool", async () => { - const { swapXAMOStrategy, strategist } = fixture; - - const tx = swapXAMOStrategy.connect(strategist).swapOTokensToPool(0); - - await expect(tx).to.be.revertedWith("Must swap something"); - }); - it("Strategist should fail to add too much OS to the pool", async () => { - const { swapXAMOStrategy, strategist } = fixture; - - // Add OS to the pool - const tx = swapXAMOStrategy - .connect(strategist) - .swapOTokensToPool(parseUnits("11000")); - - await expect(tx).to.be.revertedWith("OTokens overshot peg"); - }); - it("Strategist should fail to add more wS to the pool", async () => { - const { swapXAMOStrategy, strategist } = fixture; - - // try swapping wS into the pool - const tx = swapXAMOStrategy - .connect(strategist) - .swapAssetsToPool(parseUnits("0.0001")); - - await expect(tx).to.be.revertedWith("Assets balance worse"); - }); - }); - - describe("with the strategy owning a small percentage of the pool", () => { - const loadFixture = createFixtureLoader(swapXAMOFixture, { - wsMintAmount: 5000, - depositToStrategy: true, - balancePool: true, - }); - let dataBefore; - beforeEach(async () => { - fixture = await loadFixture(); - - const { clement, wS, oSonic, oSonicVault, swapXPool } = fixture; - - // Other users adds a lot more liquidity to the pool - const bigAmount = parseUnits("1000000"); - // transfer wS to the pool - await wS.connect(clement).transfer(swapXPool.address, bigAmount); - // Mint OS with wS - await oSonicVault.connect(clement).mint(wS.address, bigAmount.mul(5), 0); - // transfer OS to the pool - await oSonic.connect(clement).transfer(swapXPool.address, bigAmount); - // mint pool LP tokens - await swapXPool.connect(clement).mint(clement.address); - - dataBefore = await snapData(); - await logSnapData(dataBefore); - }); - it("a lot of OS is swapped into the pool", async () => { - const { oSonic, swapXAMOStrategy, wS } = fixture; - - // Swap OS into the pool and wS out - await poolSwapTokensIn(oSonic, parseUnits("1005000")); - - await logSnapData(await snapData(), "\nAfter swapping OS into the pool"); - - // Assert the strategy's balance - expect( - await swapXAMOStrategy.checkBalance(wS.address), - "Strategy's check balance" - ).to.withinRange(dataBefore.stratBalance, dataBefore.stratBalance.add(1)); - - // Swap wS into the pool and OS out - await poolSwapTokensIn(wS, parseUnits("2000000")); - - await logSnapData(await snapData(), "\nAfter swapping wS into the pool"); - - // Assert the strategy's balance - expect( - await swapXAMOStrategy.checkBalance(wS.address), - "Strategy's check balance" - ).to.withinRange(dataBefore.stratBalance, dataBefore.stratBalance.add(1)); - }); - it("a lot of wS is swapped into the pool", async () => { - const { swapXAMOStrategy, oSonic, wS } = fixture; - - // Swap wS into the pool and OS out - await poolSwapTokensIn(wS, parseUnits("1006000")); - - await logSnapData(await snapData(), "\nAfter swapping wS into the pool"); - - // Assert the strategy's balance - expect( - await swapXAMOStrategy.checkBalance(wS.address), - "Strategy's check balance" - ).to.withinRange(dataBefore.stratBalance, dataBefore.stratBalance.add(1)); - - // Swap OS into the pool and wS out - await poolSwapTokensIn(oSonic, parseUnits("1005000")); - - await logSnapData(await snapData(), "\nAfter swapping OS into the pool"); - - // Assert the strategy's balance - expect( - await swapXAMOStrategy.checkBalance(wS.address), - "Strategy's check balance" - ).to.withinRange(dataBefore.stratBalance, dataBefore.stratBalance.add(1)); - }); - }); - - describe("with an insolvent vault", () => { - const loadFixture = createFixtureLoader(swapXAMOFixture, { - wsMintAmount: 5000000, - depositToStrategy: false, - }); - beforeEach(async () => { - fixture = await loadFixture(); - - const { oSonicVault, oSonicVaultSigner, swapXAMOStrategy, wS } = fixture; - - // Deposit a little to the strategy - const littleAmount = parseUnits("100"); - await wS - .connect(oSonicVaultSigner) - .transfer(swapXAMOStrategy.address, littleAmount); - await swapXAMOStrategy - .connect(oSonicVaultSigner) - .deposit(wS.address, littleAmount); - - const totalAssets = await oSonicVault.totalValue(); - // Calculate a 0.21% (21 basis points) loss - const lossAmount = totalAssets.mul(21).div(10000); - await wS.connect(oSonicVaultSigner).transfer(addresses.dead, lossAmount); - expect( - await wS.balanceOf(oSonicVault.address), - "Must have enough wS in vault to make insolvent" - ).to.gte(lossAmount); - }); - it("Should fail to deposit", async () => { - const { oSonicVaultSigner, swapXAMOStrategy, wS } = fixture; - - // Vault calls deposit on the strategy - const depositAmount = parseUnits("10"); - await wS - .connect(oSonicVaultSigner) - .transfer(swapXAMOStrategy.address, depositAmount); - const tx = swapXAMOStrategy - .connect(oSonicVaultSigner) - .deposit(wS.address, depositAmount); - - await expect(tx).to.be.revertedWith("Protocol insolvent"); - }); - it("Should fail to withdraw", async () => { - const { oSonicVault, oSonicVaultSigner, swapXAMOStrategy, wS } = fixture; - - // Vault withdraws from the strategy - const tx = swapXAMOStrategy - .connect(oSonicVaultSigner) - .withdraw(oSonicVault.address, wS.address, parseUnits("10")); - - await expect(tx).to.be.revertedWith("Protocol insolvent"); - }); - it("Should withdraw all", async () => { - const { oSonicVaultSigner, swapXAMOStrategy } = fixture; - - // Vault withdraw alls from the strategy - const tx = swapXAMOStrategy.connect(oSonicVaultSigner).withdrawAll(); - - await expect(tx).to.not.revertedWith("Protocol insolvent"); - }); - it("Should fail to swap assets to the pool", async () => { - const { swapXAMOStrategy, strategist } = fixture; - - const tx = swapXAMOStrategy - .connect(strategist) - .swapAssetsToPool(parseUnits("10")); - - await expect(tx).to.be.revertedWith("Protocol insolvent"); - }); - it("Should fail to swap OS to the pool", async () => { - const { swapXAMOStrategy, strategist } = fixture; - - const tx = swapXAMOStrategy - .connect(strategist) - .swapOTokensToPool(parseUnits("10")); - - await expect(tx).to.be.revertedWith("Protocol insolvent"); - }); - }); - - const poolSwapTokensIn = async (tokenIn, amountIn) => { - const { clement, swapXPool, wS } = fixture; - const amountOut = await swapXPool.getAmountOut(amountIn, tokenIn.address); - await tokenIn.connect(clement).transfer(swapXPool.address, amountIn); - if (tokenIn.address == wS.address) { - await swapXPool.swap(0, amountOut, clement.address, "0x"); - } else { - await swapXPool.swap(amountOut, 0, clement.address, "0x"); - } - - return amountOut; - }; - - const precision = parseUnits("1", 18); - // Calculate the value of wS and OS tokens assuming the pool is balanced - const calcReserveValue = (reserves) => { - const k = calcInvariant(reserves); - - // If x = y, let’s denote x = y = z (where z is the common reserve value) - // Substitute z into the invariant: - // k = z^3 * z + z * z^3 - // k = 2 * z^4 - // Going back the other way to calculate the common reserve value z - // z = (k / 2) ^ (1/4) - // the total value of the pool when x = y is 2 * z, which is 2 * (k / 2) ^ (1/4) - const zSquared = sqrt(k.mul(precision).div(2)); - const z = sqrt(zSquared.mul(precision)); - return z.mul(2); - }; - - const calcInvariant = (reserves) => { - const x = reserves.ws; - const y = reserves.os; - const a = x.mul(y).div(precision); - const b = x.mul(x).div(precision).add(y.mul(y).div(precision)); - const k = a.mul(b).div(precision); - - return k; - }; - - // Babylonian square root function for Ethers.js BigNumber - function sqrt(value) { - // Convert input to BigNumber if it isn't already - let bn = ethers.BigNumber.from(value); - - // Handle edge cases - if (bn.lt(0)) { - throw new Error("Square root of negative number is not supported"); - } - if (bn.eq(0)) { - return ethers.BigNumber.from(0); - } - - // Initial guess (number / 2) - let guess = bn.div(2); - - // Define precision threshold (in wei scale, 10^-18) - const epsilon = ethers.BigNumber.from("1"); // 1 wei precision - - // Keep refining until we reach desired precision - while (true) { - // Babylonian method: nextGuess = (guess + number/guess) / 2 - // Using mul and div for BigNumber arithmetic - let numerator = guess.add(bn.div(guess)); - let nextGuess = numerator.div(2); - - // Calculate absolute difference - let diff = nextGuess.gt(guess) - ? nextGuess.sub(guess) - : guess.sub(nextGuess); - - // If difference is less than epsilon, we're done - if (diff.lte(epsilon)) { - return nextGuess; - } - - // Update guess for next iteration - guess = nextGuess; - } - } - - const snapData = async () => { - const { oSonicVault, swapXAMOStrategy, oSonic, swapXPool, swapXGauge, wS } = - fixture; - - const stratBalance = await swapXAMOStrategy.checkBalance(wS.address); - const osSupply = await oSonic.totalSupply(); - const vaultAssets = await oSonicVault.totalValue(); - const poolSupply = await swapXPool.totalSupply(); - const { _reserve0: wsReserves, _reserve1: osReserves } = - await swapXPool.getReserves(); - const reserves = { ws: wsReserves, os: osReserves }; - // Amount of wS bought from selling 1 OS - const wsAmount = await swapXPool.getAmountOut( - parseUnits("1"), - oSonic.address - ); - // OS/wS price = wS / OS - const sellPrice = wsAmount; - - // Amount of wS sold from buying 1 OS - const osAmount = await swapXPool.getAmountOut(parseUnits("1"), wS.address); - // OS/wS price = wS / OS - const buyPrice = parseUnits("1", 36).div(osAmount); - - const k = calcInvariant(reserves); - const stratGaugeBalance = await swapXGauge.balanceOf( - swapXAMOStrategy.address - ); - const gaugeSupply = await swapXGauge.totalSupply(); - const vaultWSBalance = await wS.balanceOf(oSonicVault.address); - const stratWSBalance = await wS.balanceOf(swapXAMOStrategy.address); +const { shouldBehaveLikeAlgebraAmoStrategy } = require("../../behaviour/algebraAmoStrategy"); +const { createFixtureLoader } = require("../../_fixture"); +describe.only("Sonic Fork Test: Sonic Staking Strategy", function () { + shouldBehaveLikeAlgebraAmoStrategy(async() => { + const fixture = await swapXAMOFixture(); return { - stratBalance, - osSupply, - vaultAssets, - poolSupply, - reserves: { ws: wsReserves, os: osReserves }, - buyPrice, - sellPrice, - stratGaugeBalance, - gaugeSupply, - vaultWSBalance, - stratWSBalance, - k, - }; - }; - - const logSnapData = async (data, message) => { - const totalReserves = data.reserves.ws.add(data.reserves.os); - const reserversPercentage = { - ws: data.reserves.ws.mul(10000).div(totalReserves), - os: data.reserves.os.mul(10000).div(totalReserves), - }; - const gaugePercentage = data.gaugeSupply.eq(0) - ? 0 - : data.stratGaugeBalance.mul(10000).div(data.gaugeSupply); - if (message) { - log(message); - } - log(`Strategy balance : ${formatUnits(data.stratBalance)}`); - log(`OS supply : ${formatUnits(data.osSupply)}`); - log(`Vault assets : ${formatUnits(data.vaultAssets)}`); - log(`pool supply : ${formatUnits(data.poolSupply)}`); - log( - `reserves wS : ${formatUnits(data.reserves.ws)} ${formatUnits( - reserversPercentage.ws, - 2 - )}%` - ); - log( - `reserves OS : ${formatUnits(data.reserves.os)} ${formatUnits( - reserversPercentage.os, - 2 - )}%` - ); - log(`buy price : ${formatUnits(data.buyPrice)} OS/wS`); - log(`sell price : ${formatUnits(data.sellPrice)} OS/wS`); - log(`Invariant K : ${formatUnits(data.k)}`); - log( - `strat gauge balance : ${formatUnits( - data.stratGaugeBalance - )} ${formatUnits(gaugePercentage, 2)}%` - ); - log(`gauge supply : ${formatUnits(data.gaugeSupply)}`); - log(`vault wS balance : ${formatUnits(data.vaultWSBalance)}`); - }; - - const logProfit = async (dataBefore) => { - const { oSonic, oSonicVault, swapXAMOStrategy, wS } = fixture; - - const stratBalanceAfter = await swapXAMOStrategy.checkBalance(wS.address); - const osSupplyAfter = await oSonic.totalSupply(); - const vaultAssetsAfter = await oSonicVault.totalValue(); - const profit = vaultAssetsAfter - .sub(dataBefore.vaultAssets) - .add(dataBefore.osSupply.sub(osSupplyAfter)); - - log( - `Change strat balance: ${formatUnits( - stratBalanceAfter.sub(dataBefore.stratBalance) - )}` - ); - log( - `Change vault assets : ${formatUnits( - vaultAssetsAfter.sub(dataBefore.vaultAssets) - )}` - ); - log( - `Change OS supply : ${formatUnits( - osSupplyAfter.sub(dataBefore.osSupply) - )}` - ); - log(`Profit : ${formatUnits(profit)}`); - - return profit; - }; - - const assertChangedData = async (dataBefore, delta) => { - const { oSonic, oSonicVault, swapXAMOStrategy, swapXPool, swapXGauge, wS } = - fixture; - - if (delta.stratBalance != undefined) { - const expectedStratBalance = dataBefore.stratBalance.add( - delta.stratBalance - ); - log(`Expected strategy balance: ${formatUnits(expectedStratBalance)}`); - - expect(await swapXAMOStrategy.checkBalance(wS.address)).to.withinRange( - expectedStratBalance.sub(15), - expectedStratBalance.add(15), - "Strategy's check balance" - ); - } - - if (delta.osSupply != undefined) { - const expectedSupply = dataBefore.osSupply.add(delta.osSupply); - expect(await oSonic.totalSupply(), "OSonic total supply").to.equal( - expectedSupply - ); - } - - // Check Vault's wS balance - if (delta.vaultWSBalance != undefined) { - expect(await wS.balanceOf(oSonicVault.address)).to.equal( - dataBefore.vaultWSBalance.add(delta.vaultWSBalance), - "Vault's wS balance" - ); - } - - // Check the pool's reserves - if (delta.reserves != undefined) { - const { _reserve0: wsReserves, _reserve1: osReserves } = - await swapXPool.getReserves(); - - // If the wS reserves delta is a function, call it to check the wS reserves - if (typeof delta.reserves.ws == "function") { - // Call test function to check the wS reserves - delta.reserves.ws(wsReserves); - } else { - expect(wsReserves, "wS reserves").to.equal( - dataBefore.reserves.ws.add(delta.reserves.ws) - ); - } - // Check OS reserves delta - expect(osReserves, "OS reserves").to.equal( - dataBefore.reserves.os.add(delta.reserves.os) - ); - } - - if (delta.stratGaugeBalance) { - // Check the strategy's gauge balance - const expectedStratGaugeBalance = dataBefore.stratGaugeBalance.add( - delta.stratGaugeBalance - ); - expect( - await swapXGauge.balanceOf(swapXAMOStrategy.address) - ).to.withinRange( - expectedStratGaugeBalance.sub(1), - expectedStratGaugeBalance.add(1), - "Strategy's gauge balance" - ); - } - }; - - async function assertDeposit(wsDepositAmount) { - const { - clement, - swapXAMOStrategy, - oSonic, - oSonicVault, - swapXPool, - oSonicVaultSigner, - wS, - } = fixture; - - await oSonicVault.connect(clement).mint(wS.address, wsDepositAmount, 0); - - const dataBefore = await snapData(); - await logSnapData(dataBefore, "\nBefore depositing wS to strategy"); - - const { lpMintAmount, osMintAmount } = await calcOSMintAmount( - wsDepositAmount - ); - - // Vault transfers wS to strategy - await wS - .connect(oSonicVaultSigner) - .transfer(swapXAMOStrategy.address, wsDepositAmount); - // Vault calls deposit on the strategy - const tx = await swapXAMOStrategy - .connect(oSonicVaultSigner) - .deposit(wS.address, wsDepositAmount); - - await logSnapData( - await snapData(), - `\nAfter depositing ${formatUnits(wsDepositAmount)} wS to strategy` - ); - await logProfit(dataBefore); - - // Check emitted events - await expect(tx).to.emit(swapXAMOStrategy, "Deposit") - .withArgs(wS.address, swapXPool.address, wsDepositAmount); - await expect(tx).to.emit(swapXAMOStrategy, "Deposit") - .withArgs(oSonic.address, swapXPool.address, osMintAmount); - - // Calculate the value of the wS and OS tokens added to the pool if the pool was balanced - const depositValue = calcReserveValue({ - ws: wsDepositAmount, - os: osMintAmount, - }); - log(`Value of deposit: ${formatUnits(depositValue)}`); - - await assertChangedData(dataBefore, { - stratBalance: depositValue, - osSupply: osMintAmount, - reserves: { ws: wsDepositAmount, os: osMintAmount }, - vaultWSBalance: wsDepositAmount.mul(-1), - gaugeSupply: lpMintAmount, - }); - - expect( - await swapXPool.balanceOf(swapXAMOStrategy.address), - "Strategy's pool LP balance" - ).to.equal(0); - expect( - await oSonic.balanceOf(swapXAMOStrategy.address), - "Strategy's OS balance" - ).to.equal(0); - } - - async function assertFailedDeposit(wsDepositAmount, errorMessage) { - const { wS, oSonicVaultSigner, swapXAMOStrategy } = fixture; - - const dataBefore = await snapData(); - await logSnapData(dataBefore, "\nBefore depositing wS to strategy"); - - // Vault transfers wS to strategy - await wS - .connect(oSonicVaultSigner) - .transfer(swapXAMOStrategy.address, wsDepositAmount); - - // Vault calls deposit on the strategy - const tx = swapXAMOStrategy - .connect(oSonicVaultSigner) - .deposit(wS.address, wsDepositAmount); - - await expect(tx, "deposit to strategy").to.be.revertedWith(errorMessage); - } - - async function assertFailedDepositAll(wsDepositAmount, errorMessage) { - const { wS, oSonicVaultSigner, swapXAMOStrategy } = fixture; - - const dataBefore = await snapData(); - await logSnapData(dataBefore, "\nBefore depositing all wS to strategy"); - - // Vault transfers wS to strategy - await wS - .connect(oSonicVaultSigner) - .transfer(swapXAMOStrategy.address, wsDepositAmount); - - // Vault calls depositAll on the strategy - const tx = swapXAMOStrategy.connect(oSonicVaultSigner).depositAll(); - - await expect(tx, "depositAll to strategy").to.be.revertedWith(errorMessage); - } - - async function assertWithdrawAll() { - const { swapXAMOStrategy, swapXPool, oSonic, oSonicVaultSigner, wS } = - fixture; - - const dataBefore = await snapData(); - await logSnapData(dataBefore); - - const { osBurnAmount, wsWithdrawAmount } = await calcWithdrawAllAmounts(); - - // Now try to withdraw all the wS from the strategy - const tx = await swapXAMOStrategy.connect(oSonicVaultSigner).withdrawAll(); - - await logSnapData(await snapData(), "\nAfter full withdraw"); - await logProfit(dataBefore); - - // Check emitted events - await expect(tx) - .to.emit(swapXAMOStrategy, "Withdrawal") - .withArgs(wS.address, swapXPool.address, wsWithdrawAmount); - await expect(tx) - .to.emit(swapXAMOStrategy, "Withdrawal") - .withArgs(oSonic.address, swapXPool.address, osBurnAmount); - - // Calculate the value of the wS and OS tokens removed from the pool if the pool was balanced - const withdrawValue = calcReserveValue({ - ws: wsWithdrawAmount, - os: osBurnAmount, - }); - - await assertChangedData(dataBefore, { - stratBalance: withdrawValue.mul(-1), - osSupply: osBurnAmount.mul(-1), - reserves: { ws: wsWithdrawAmount.mul(-1), os: osBurnAmount.mul(-1) }, - vaultWSBalance: wsWithdrawAmount, - stratGaugeBalance: dataBefore.stratGaugeBalance.mul(-1), - }); - - expect( - wsWithdrawAmount.add(osBurnAmount), - "wS withdraw and OS burnt >= strategy balance" - ).to.gte(dataBefore.stratBalance); - - expect( - await swapXPool.balanceOf(swapXAMOStrategy.address), - "Strategy's pool LP balance" - ).to.equal(0); - expect( - await oSonic.balanceOf(swapXAMOStrategy.address), - "Strategy's OS balance" - ).to.equal(0); - } - - async function assertWithdrawPartial(wsWithdrawAmount) { - const { - swapXAMOStrategy, - oSonic, - swapXPool, - oSonicVault, - oSonicVaultSigner, - wS, - } = fixture; - - const dataBefore = await snapData(); - - const { lpBurnAmount, osBurnAmount } = await calcOSWithdrawAmount( - wsWithdrawAmount - ); - - // Now try to withdraw the wS from the strategy - const tx = await swapXAMOStrategy - .connect(oSonicVaultSigner) - .withdraw(oSonicVault.address, wS.address, wsWithdrawAmount); - - await logSnapData( - await snapData(), - `\nAfter withdraw of ${formatUnits(wsWithdrawAmount)}` - ); - await logProfit(dataBefore); - - // Check emitted events - await expect(tx) - .to.emit(swapXAMOStrategy, "Withdrawal") - .withArgs(wS.address, swapXPool.address, wsWithdrawAmount); - await expect(tx).to.emit(swapXAMOStrategy, "Withdrawal").withNamedArgs({ - _asset: oSonic.address, - _pToken: swapXPool.address, - }); - - // Calculate the value of the wS and OS tokens removed from the pool if the pool was balanced - const withdrawValue = calcReserveValue({ - ws: wsWithdrawAmount, - os: osBurnAmount, - }); - - await assertChangedData(dataBefore, { - stratBalance: withdrawValue.mul(-1), - osSupply: osBurnAmount.mul(-1), - reserves: { - ws: (actualWsReserve) => { - const expectedWsReserves = - dataBefore.reserves.ws.sub(wsWithdrawAmount); - - expect(actualWsReserve).to.withinRange( - expectedWsReserves.sub(50), - expectedWsReserves, - "wS reserves" - ); - }, - os: osBurnAmount.mul(-1), - }, - vaultWSBalance: wsWithdrawAmount, - gaugeSupply: lpBurnAmount.mul(-1), - }); - - expect( - await swapXPool.balanceOf(swapXAMOStrategy.address), - "Strategy's pool LP balance" - ).to.equal(0); - expect( - await oSonic.balanceOf(swapXAMOStrategy.address), - "Strategy's OS balance" - ).to.equal(0); - } - - async function assertSwapAssetsToPool(wsAmount) { - const { oSonic, swapXAMOStrategy, swapXPool, strategist, wS } = fixture; - - const dataBefore = await snapData(); - await logSnapData( - dataBefore, - `Before swapping ${formatUnits(wsAmount)} wS into the pool` - ); - - const { lpBurnAmount: expectedLpBurnAmount, osBurnAmount: osBurnAmount1 } = - await calcOSWithdrawAmount(wsAmount); - // TODO this is not accurate as the liquidity needs to be removed first - const osBurnAmount2 = await swapXPool.getAmountOut(wsAmount, wS.address); - const osBurnAmount = osBurnAmount1.add(osBurnAmount2); - - // Swap wS to the pool and burn the received OS from the pool - const tx = await swapXAMOStrategy - .connect(strategist) - .swapAssetsToPool(wsAmount); - - await logSnapData(await snapData(), "\nAfter swapping assets to the pool"); - await logProfit(dataBefore); - - // Check emitted event - await expect(tx).to.emittedEvent("SwapAssetsToPool", [ - (actualWsAmount) => { - expect(actualWsAmount).to.withinRange( - wsAmount.sub(1), - wsAmount.add(1), - "SwapAssetsToPool event wsAmount" - ); - }, - expectedLpBurnAmount, - (actualOsBurnAmount) => { - // TODO this can be tightened once osBurnAmount is more accurately calculated - expect(actualOsBurnAmount).to.approxEqualTolerance( - osBurnAmount, - 10, - "SwapAssetsToPool event osBurnt" - ); - }, - ]); - - await assertChangedData( - dataBefore, - { - // stratBalance: osBurnAmount.mul(-1), - vaultWSBalance: 0, - stratGaugeBalance: 0, - }, - fixture - ); - - expect( - await swapXPool.balanceOf(swapXAMOStrategy.address), - "Strategy's pool LP balance" - ).to.equal(0); - expect( - await oSonic.balanceOf(swapXAMOStrategy.address), - "Strategy's OS balance" - ).to.equal(0); - } - - async function assertSwapOTokensToPool(osAmount) { - const { oSonic, swapXPool, swapXAMOStrategy, strategist } = fixture; - - const dataBefore = await snapData(); - await logSnapData(dataBefore, "Before swapping OTokens to the pool"); - - // Mint OS and swap into the pool, then mint more OS to add with the wS swapped out - const tx = await swapXAMOStrategy - .connect(strategist) - .swapOTokensToPool(osAmount); - - // Check emitted event - await expect(tx) - .emit(swapXAMOStrategy, "SwapOTokensToPool") - .withNamedArgs({ oTokenMinted: osAmount }); - - await logSnapData(await snapData(), "\nAfter swapping OTokens to the pool"); - await logProfit(dataBefore); - - await assertChangedData( - dataBefore, - { - // stratBalance: osBurnAmount.mul(-1), - vaultWSBalance: 0, - stratGaugeBalance: 0, + fixture: swapXAMOFixture, + loadFixture: (config = {}) => { + return createFixtureLoader(swapXAMOFixture, config); }, - fixture - ); - - expect( - await swapXPool.balanceOf(swapXAMOStrategy.address), - "Strategy's pool LP balance" - ).to.equal(0); - expect( - await oSonic.balanceOf(swapXAMOStrategy.address), - "Strategy's OS balance" - ).to.equal(0); - } - - // Calculate the minted OS amount for a deposit - async function calcOSMintAmount(wsDepositAmount) { - const { swapXPool } = fixture; - - // Get the reserves of the pool - const { _reserve0: wsReserves, _reserve1: osReserves } = - await swapXPool.getReserves(); - - const osMintAmount = wsDepositAmount.mul(osReserves).div(wsReserves); - log(`OS mint amount : ${formatUnits(osMintAmount)}`); - - const lpTotalSupply = await swapXPool.totalSupply(); - const lpMintAmount = wsDepositAmount.mul(lpTotalSupply).div(wsReserves); - - return { lpMintAmount, osMintAmount }; - } - - // Calculate the amount of OS burnt from a withdraw - async function calcOSWithdrawAmount(wsWithdrawAmount) { - const { swapXPool } = fixture; - - // Get the reserves of the pool - const { _reserve0: wsReserves, _reserve1: osReserves } = - await swapXPool.getReserves(); - - // lp tokens to burn = wS withdrawn * total LP supply / wS pool balance - const totalLpSupply = await swapXPool.totalSupply(); - const lpBurnAmount = wsWithdrawAmount - .mul(totalLpSupply) - .div(wsReserves) - .add(1); - // OS to burn = LP tokens to burn * OS reserves / total LP supply - const osBurnAmount = lpBurnAmount.mul(osReserves).div(totalLpSupply); - - log(`OS burn amount : ${formatUnits(osBurnAmount)}`); - - return { lpBurnAmount, osBurnAmount }; - } - - // Calculate the OS and wS amounts from a withdrawAll - async function calcWithdrawAllAmounts() { - const { swapXAMOStrategy, swapXGauge, swapXPool } = fixture; - - // Get the reserves of the pool - const { _reserve0: wsReserves, _reserve1: osReserves } = - await swapXPool.getReserves(); - const strategyLpAmount = await swapXGauge.balanceOf( - swapXAMOStrategy.address - ); - const totalLpSupply = await swapXPool.totalSupply(); - - // wS to withdraw = wS pool balance * strategy LP amount / total pool LP amount - const wsWithdrawAmount = wsReserves - .mul(strategyLpAmount) - .div(totalLpSupply); - // OS to burn = OS pool balance * strategy LP amount / total pool LP amount - const osBurnAmount = osReserves.mul(strategyLpAmount).div(totalLpSupply); - - log(`wS withdraw amount : ${formatUnits(wsWithdrawAmount)}`); - log(`OS burn amount : ${formatUnits(osBurnAmount)}`); - - return { - wsWithdrawAmount, - osBurnAmount, + addresses: addresses.sonic, + assetToken: fixture.wS, // address of the asset token in the pool + oToken: fixture.oSonic, // address of the oToken in the pool + amoStrategy: fixture.swapXAMOStrategy, // address of the strategy + pool: fixture.swapXPool, + gauge: fixture.swapXGauge, // address of the gauge + governor: fixture.governor, // address of the governor + timelock: fixture.governor, // address of the timelock + strategist: fixture.strategist, // address of the strategist + nick: fixture.nick, // nick's address + oTokenPoolIndex: 1, // index of the oToken in the pool + vaultSigner: fixture.oSonicVaultSigner, // address of the vault signer + vault: fixture.oSonicVault, // address of the vault }; - } -}); + }); +}); \ No newline at end of file From adc26455792d4a0471bcb5f9156c7aa7cbe5b20a Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 16 Feb 2026 02:25:41 +0100 Subject: [PATCH 09/44] migrate some more tests --- .../test/behaviour/algebraAmoStrategy.js | 595 +++++++++--------- .../sonic/swapx-amo.sonic.fork-test.js | 16 +- 2 files changed, 309 insertions(+), 302 deletions(-) diff --git a/contracts/test/behaviour/algebraAmoStrategy.js b/contracts/test/behaviour/algebraAmoStrategy.js index baf402d8fe..cafa97bbb0 100644 --- a/contracts/test/behaviour/algebraAmoStrategy.js +++ b/contracts/test/behaviour/algebraAmoStrategy.js @@ -1,6 +1,6 @@ const { expect } = require("chai"); const { formatUnits, parseUnits } = require("ethers/lib/utils"); - +const { impersonateAndFund } = require("../../utils/signers"); const { isCI } = require("../helpers"); const log = require("../../utils/logger")("test:fork:algebra:amo"); @@ -22,18 +22,18 @@ const log = require("../../utils/logger")("test:fork:algebra:amo"); vaultSigner: addresses.sonic.vaultSigner, // address of the vault signer })); */ -const shouldBehaveLikeAlgebraAmoStrategy = (context) => { - describe("ForkTest: Algebra AMO Strategy", function () { +const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { + describe("ForkTest: Algebra AMO Strategy", async function () { // Retry up to 3 times on CI this.retries(isCI ? 3 : 0); - let fixture; + let fixture, context; describe("post deployment", () => { beforeEach(async () => { - const { loadFixture } = await context(); - fixture = await loadFixture(); + context = await contextFunction(); + fixture = await context.loadFixture(); }); it("Should have constants and immutables set", async () => { - const { amoStrategy, assetToken, oToken, pool, gauge, governor } = await context(); + const { amoStrategy, assetToken, oToken, pool, gauge, governor } = context; expect(await amoStrategy.SOLVENCY_THRESHOLD()).to.equal( parseUnits("0.998", 18) @@ -55,7 +55,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (context) => { expect(await amoStrategy.maxDepeg()).to.equal(parseUnits("0.01")); }); it("Should be able to check balance", async () => { - const { assetToken, nick, amoStrategy } = await context(); + const { assetToken, nick, amoStrategy } = context; const balance = await amoStrategy.checkBalance(assetToken.address); log(`check balance ${balance}`); @@ -75,7 +75,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (context) => { vaultSigner, amoStrategy, pool, - } = await context(); + } = context; expect(await amoStrategy.connect(timelock).isGovernor()).to.equal( true @@ -99,7 +99,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (context) => { nick, vaultSigner, amoStrategy, - } = await context(); + } = context; expect(await amoStrategy.connect(timelock).isGovernor()).to.equal( true @@ -123,19 +123,19 @@ const shouldBehaveLikeAlgebraAmoStrategy = (context) => { }); }); - describe("with wS in the vault", () => { + describe("with asset token in the vault", () => { beforeEach(async () => { - const { loadFixture } = await context({ - wsMintAmount: 5000000, + context = await contextFunction(); + fixture = await context.loadFixture({ + assetMintAmount: 5000000, depositToStrategy: false, balancePool: true, }); - fixture = await loadFixture(); }); - it.only("Vault should deposit wS to AMO strategy", async function () { + it("Vault should deposit asset token to AMO strategy", async function () { await assertDeposit(parseUnits("2000")); }); - it("Only vault can deposit wS to AMO strategy", async function () { + it("Only vault can deposit asset token to AMO strategy", async function () { const { amoStrategy, vaultSigner, @@ -143,7 +143,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (context) => { timelock, nick, assetToken, - } = await context(); + } = context; const depositAmount = parseUnits("50"); await assetToken @@ -167,7 +167,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (context) => { timelock, nick, assetToken, - } = await context(); + } = context; const depositAmount = parseUnits("50"); await assetToken @@ -187,221 +187,222 @@ const shouldBehaveLikeAlgebraAmoStrategy = (context) => { }); }); - // describe("with the strategy having OS and wS in a balanced pool", () => { - // const loadFixture = createFixtureLoader(swapXAMOFixture, { - // wsMintAmount: 100000, - // depositToStrategy: true, - // balancePool: true, - // }); - // beforeEach(async () => { - // fixture = await loadFixture(); - // }); - // it("Vault should deposit wS", async function () { - // await assertDeposit(parseUnits("5000")); - // }); - // it("Vault should be able to withdraw all", async () => { - // await assertWithdrawAll(); - // }); - // it("Vault should be able to withdraw all in SwapX Emergency", async () => { - // const { amoStrategy, swapXGauge, vaultSigner } = fixture; + describe("with the strategy having OToken and asset token in a balanced pool", () => { + beforeEach(async () => { + context = await contextFunction(); + fixture = await context.loadFixture({ + assetMintAmount: 5000000, + depositToStrategy: true, + balancePool: true, + }); + }); + it("Vault should deposit asset token", async function () { + await assertDeposit(parseUnits("5000")); + }); + it("Vault should be able to withdraw all", async () => { + await assertWithdrawAll(); + }); + it("Vault should be able to withdraw all in SwapX Emergency", async () => { + const { amoStrategy, gauge, vaultSigner } = context; - // const gaugeOwner = await swapXGauge.owner(); - // const ownerSigner = await impersonateAndFund(gaugeOwner); - // await swapXGauge.connect(ownerSigner).activateEmergencyMode(); - // await assertWithdrawAll(); + const gaugeOwner = await gauge.owner(); + const ownerSigner = await impersonateAndFund(gaugeOwner); + await gauge.connect(ownerSigner).activateEmergencyMode(); + await assertWithdrawAll(); - // // Try again when the strategy is empty - // await amoStrategy.connect(vaultSigner).withdrawAll(); - // }); - // it("Should fail to deposit zero wS", async () => { - // const { amoStrategy, vaultSigner, wS } = fixture; + // Try again when the strategy is empty + await amoStrategy.connect(vaultSigner).withdrawAll(); + }); + it("Should fail to deposit zero asset token", async () => { + const { amoStrategy, vaultSigner, assetToken } = context; - // const tx = amoStrategy - // .connect(vaultSigner) - // .deposit(assetToken.address, 0); + const tx = amoStrategy + .connect(vaultSigner) + .deposit(assetToken.address, 0); - // await expect(tx).to.be.revertedWith("Must deposit something"); - // }); - // it("Should fail to deposit OS", async () => { - // const { amoStrategy, vaultSigner, oSonic } = fixture; + await expect(tx).to.be.revertedWith("Must deposit something"); + }); + it("Should fail to deposit oToken", async () => { + const { amoStrategy, vaultSigner, oToken } = context; - // const tx = amoStrategy - // .connect(vaultSigner) - // .deposit(oSonic.address, parseUnits("1")); + const tx = amoStrategy + .connect(vaultSigner) + .deposit(oToken.address, parseUnits("1")); - // await expect(tx).to.be.revertedWith("Unsupported asset"); - // }); - // it("Should fail to withdraw zero wS", async () => { - // const { amoStrategy, vaultSigner, oSonicVault, wS } = fixture; + await expect(tx).to.be.revertedWith("Unsupported asset"); + }); + it("Should fail to withdraw zero asset token", async () => { + const { amoStrategy, vaultSigner, vault, assetToken } = context; - // const tx = amoStrategy - // .connect(vaultSigner) - // .withdraw(oSonicVault.address, assetToken.address, 0); + const tx = amoStrategy + .connect(vaultSigner) + .withdraw(vault.address, assetToken.address, 0); - // await expect(tx).to.be.revertedWith("Must withdraw something"); - // }); - // it("Should fail to withdraw OS", async () => { - // const { amoStrategy, vaultSigner, oSonic, oSonicVault } = - // fixture; + await expect(tx).to.be.revertedWith("Must withdraw something"); + }); + it("Should fail to withdraw oToken", async () => { + const { amoStrategy, vaultSigner, oToken, vault } = + context; - // const tx = amoStrategy - // .connect(vaultSigner) - // .withdraw(oSonicVault.address, oSonic.address, parseUnits("1")); + const tx = amoStrategy + .connect(vaultSigner) + .withdraw(vault.address, oToken.address, parseUnits("1")); - // await expect(tx).to.be.revertedWith("Unsupported asset"); - // }); - // it("Should fail to withdraw to a user", async () => { - // const { amoStrategy, vaultSigner, wS, nick } = fixture; + await expect(tx).to.be.revertedWith("Unsupported asset"); + }); + it("Should fail to withdraw to a user", async () => { + const { amoStrategy, vaultSigner, assetToken, nick } = context; - // const tx = amoStrategy - // .connect(vaultSigner) - // .withdraw(nick.address, assetToken.address, parseUnits("1")); + const tx = amoStrategy + .connect(vaultSigner) + .withdraw(nick.address, assetToken.address, parseUnits("1")); - // await expect(tx).to.be.revertedWith("Only withdraw to vault allowed"); - // }); - // it("Vault should be able to withdraw all from empty strategy", async () => { - // const { amoStrategy, vaultSigner } = fixture; - // await assertWithdrawAll(); + await expect(tx).to.be.revertedWith("Only withdraw to vault allowed"); + }); + it("Vault should be able to withdraw all from empty strategy", async () => { + const { amoStrategy, vaultSigner } = context; + await assertWithdrawAll(); - // // Now try again after all the assets have already been withdrawn - // const tx = await amoStrategy - // .connect(vaultSigner) - // .withdrawAll(); + // Now try again after all the assets have already been withdrawn + const tx = await amoStrategy + .connect(vaultSigner) + .withdrawAll(); - // // Check emitted events - // await expect(tx).to.not.emit(amoStrategy, "Withdrawal"); - // }); - // it("Vault should be able to partially withdraw", async () => { - // await assertWithdrawPartial(parseUnits("1000")); - // }); - // it("Only vault can withdraw wS from AMO strategy", async function () { - // const { amoStrategy, oSonicVault, strategist, timelock, nick, wS } = - // fixture; + // Check emitted events + await expect(tx).to.not.emit(amoStrategy, "Withdrawal"); + }); + it("Vault should be able to partially withdraw", async () => { + await assertWithdrawPartial(parseUnits("1000")); + }); + it("Only vault can withdraw asset token from AMO strategy", async function () { + const { amoStrategy, vault, strategist, timelock, nick, assetToken } = + context; - // for (const signer of [strategist, timelock, nick]) { - // const tx = amoStrategy - // .connect(signer) - // .withdraw(oSonicVault.address, assetToken.address, parseUnits("50")); + for (const signer of [strategist, timelock, nick]) { + const tx = amoStrategy + .connect(signer) + .withdraw(vault.address, assetToken.address, parseUnits("50")); - // await expect(tx).to.revertedWith("Caller is not the Vault"); - // } - // }); - // it("Only vault and governor can withdraw all WETH from AMO strategy", async function () { - // const { amoStrategy, strategist, timelock, nick } = fixture; + await expect(tx).to.revertedWith("Caller is not the Vault"); + } + }); + it("Only vault and governor can withdraw all WETH from AMO strategy", async function () { + const { amoStrategy, strategist, timelock, nick } = context; - // for (const signer of [strategist, nick]) { - // const tx = amoStrategy.connect(signer).withdrawAll(); + for (const signer of [strategist, nick]) { + const tx = amoStrategy.connect(signer).withdrawAll(); - // await expect(tx).to.revertedWith("Caller is not the Vault or Governor"); - // } + await expect(tx).to.revertedWith("Caller is not the Vault or Governor"); + } - // // Governor can withdraw all - // const tx = amoStrategy.connect(timelock).withdrawAll(); - // await expect(tx).to.emit(amoStrategy, "Withdrawal"); - // }); - // it("Harvester can collect rewards", async () => { - // const { - // harvester, - // nick, - // amoStrategy, - // swapXGauge, - // swpx, - // strategist, - // } = fixture; - - // const swpxBalanceBefore = await swpx.balanceOf(strategist.address); - - // // Send some SWPx rewards to the gauge - // const distributorAddress = await swapXGauge.DISTRIBUTION(); - // const distributorSigner = await impersonateAndFund(distributorAddress); - // const rewardAmount = parseUnits("1000"); - // await setERC20TokenBalance(distributorAddress, swpx, rewardAmount); - // await swapXGauge - // .connect(distributorSigner) - // .notifyRewardAmount(swpx.address, rewardAmount); - - // // Harvest the rewards - // // prettier-ignore - // const tx = await harvester - // .connect(nick)["harvestAndTransfer(address)"](amoStrategy.address); - - // await expect(tx).to.emit(amoStrategy, "RewardTokenCollected"); - - // const swpxBalanceAfter = await swpx.balanceOf(strategist.address); - // log( - // `Rewards collected ${formatUnits( - // swpxBalanceAfter.sub(swpxBalanceBefore) - // )} SWPx` - // ); - // expect(swpxBalanceAfter).to.gt(swpxBalanceBefore); - // }); - // it("Attacker front-run deposit within range by adding wS to the pool", async () => { - // const { nick, oSonic, vaultSigner, amoStrategy, wS } = - // fixture; - - // const attackerWsBalanceBefore = await assetToken.balanceOf(nick.address); - // const wsAmountIn = parseUnits("20000"); - - // const dataBeforeSwap = await snapData(); - // logSnapData( - // dataBeforeSwap, - // `\nBefore attacker swaps ${formatUnits( - // wsAmountIn - // )} wS into the pool for OS` - // ); - - // // Attacker swaps a lot of wS for OS in the pool - // // This drops the pool's wS/OS price and increases the OS/wS price - // const osAmountOut = await poolSwapTokensIn(wS, wsAmountIn); - - // const depositAmount = parseUnits("200000"); - - // const dataBeforeDeposit = await snapData(); - // logSnapData( - // dataBeforeDeposit, - // `\nAfter attacker tilted pool and before strategist deposits ${formatUnits( - // depositAmount - // )} wS` - // ); - - // // Vault deposits wS to the strategy - // await wS - // .connect(vaultSigner) - // .transfer(amoStrategy.address, depositAmount); - // await amoStrategy - // .connect(vaultSigner) - // .deposit(assetToken.address, depositAmount); + // Governor can withdraw all + const tx = amoStrategy.connect(timelock).withdrawAll(); + await expect(tx).to.emit(amoStrategy, "Withdrawal"); + }); + it("Harvester can collect rewards", async () => { + const { + harvester, + nick, + amoStrategy, + gauge, + swpx, + strategist, + } = context; + + const swpxBalanceBefore = await swpx.balanceOf(strategist.address); + + // Send some SWPx rewards to the gauge + const distributorAddress = await gauge.DISTRIBUTION(); + const distributorSigner = await impersonateAndFund(distributorAddress); + const rewardAmount = parseUnits("1000"); + await setERC20TokenBalance(distributorAddress, swpx, rewardAmount); + await gauge + .connect(distributorSigner) + .notifyRewardAmount(swpx.address, rewardAmount); + + // Harvest the rewards + // prettier-ignore + const tx = await harvester + .connect(nick)["harvestAndTransfer(address)"](amoStrategy.address); + + await expect(tx).to.emit(amoStrategy, "RewardTokenCollected"); + + const swpxBalanceAfter = await swpx.balanceOf(strategist.address); + log( + `Rewards collected ${formatUnits( + swpxBalanceAfter.sub(swpxBalanceBefore) + )} SWPx` + ); + expect(swpxBalanceAfter).to.gt(swpxBalanceBefore); + }); + it("Attacker front-run deposit within range by adding wS to the pool", async () => { + const { nick, oSonic, vaultSigner, amoStrategy, assetToken } = + context; + + const attackerWsBalanceBefore = await assetToken.balanceOf(nick.address); + const wsAmountIn = parseUnits("20000"); + + const dataBeforeSwap = await snapData(); + logSnapData( + dataBeforeSwap, + `\nBefore attacker swaps ${formatUnits( + wsAmountIn + )} wS into the pool for OS` + ); - // const dataAfterDeposit = await snapData(); - // logSnapData( - // dataAfterDeposit, - // `\nAfter deposit of ${formatUnits( - // depositAmount - // )} wS to strategy and before attacker swaps ${formatUnits( - // osAmountOut - // )} OS back into the pool for wS` - // ); - // await logProfit(dataBeforeSwap); - - // // Attacker swaps the OS back for wS - // await poolSwapTokensIn(oSonic, osAmountOut); - - // const dataAfterFinalSwap = await snapData(); - // logSnapData( - // dataAfterFinalSwap, - // `\nAfter attacker swaps ${formatUnits( - // osAmountOut - // )} OS back into the pool for wS` - // ); - // await logProfit(dataBeforeSwap); - - // const attackerWsBalanceAfter = await assetToken.balanceOf(nick.address); - // log( - // `Attacker's profit ${formatUnits( - // attackerWsBalanceAfter.sub(attackerWsBalanceBefore) - // )} wS` - // ); - // }); + // Attacker swaps a lot of wS for OS in the pool + // This drops the pool's wS/OS price and increases the OS/wS price + const osAmountOut = await poolSwapTokensIn(assetToken, wsAmountIn); + + const depositAmount = parseUnits("200000"); + + const dataBeforeDeposit = await snapData(); + logSnapData( + dataBeforeDeposit, + `\nAfter attacker tilted pool and before strategist deposits ${formatUnits( + depositAmount + )} wS` + ); + + // Vault deposits wS to the strategy + await assetToken + .connect(vaultSigner) + .transfer(amoStrategy.address, depositAmount); + await amoStrategy + .connect(vaultSigner) + .deposit(assetToken.address, depositAmount); + + const dataAfterDeposit = await snapData(); + logSnapData( + dataAfterDeposit, + `\nAfter deposit of ${formatUnits( + depositAmount + )} wS to strategy and before attacker swaps ${formatUnits( + osAmountOut + )} OS back into the pool for wS` + ); + await logProfit(dataBeforeSwap); + + // Attacker swaps the OS back for wS + await poolSwapTokensIn(oSonic, osAmountOut); + + const dataAfterFinalSwap = await snapData(); + logSnapData( + dataAfterFinalSwap, + `\nAfter attacker swaps ${formatUnits( + osAmountOut + )} OS back into the pool for wS` + ); + await logProfit(dataBeforeSwap); + + const attackerWsBalanceAfter = await assetToken.balanceOf(nick.address); + log( + `Attacker's profit ${formatUnits( + attackerWsBalanceAfter.sub(attackerWsBalanceBefore) + )} wS` + ); + }); + }); // TODO DELETE THIS LINE AFTER UNCOMMENT BELOW // describe("When attacker front-run by adding a lot of wS to the pool", () => { // let attackerWsBalanceBefore; // let dataBeforeSwap; @@ -1067,7 +1068,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (context) => { const snapData = async () => { const { vault, amoStrategy, oToken, pool, gauge, assetToken } = - await context(); + context; const stratBalance = await amoStrategy.checkBalance(assetToken.address); const oTokenSupply = await oToken.totalSupply(); @@ -1154,7 +1155,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (context) => { }; const logProfit = async (dataBefore) => { - const { oToken, vault, amoStrategy, assetToken } = await context(); + const { oToken, vault, amoStrategy, assetToken } = context; const stratBalanceAfter = await amoStrategy.checkBalance(assetToken.address); const oTokenSupplyAfter = await oToken.totalSupply(); @@ -1185,7 +1186,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (context) => { const assertChangedData = async (dataBefore, delta) => { const { oToken, vault, amoStrategy, pool, gauge, assetToken } = - await context(); + context; if (delta.stratBalance != undefined) { const expectedStratBalance = dataBefore.stratBalance.add( @@ -1208,23 +1209,23 @@ const shouldBehaveLikeAlgebraAmoStrategy = (context) => { } // Check Vault's asset token balance - if (delta.vaultAssetTokenBalance != undefined) { + if (delta.vaultAssetBalance != undefined) { expect(await assetToken.balanceOf(vault.address)).to.equal( - dataBefore.vaultAssetTokenBalance.add(delta.vaultAssetTokenBalance), + dataBefore.vaultAssetBalance.add(delta.vaultAssetBalance), "Vault's assetToken balance" ); } // Check the pool's reserves if (delta.reserves != undefined) { - const { assetTokenReserves, oTokenReserves } = await getPoolReserves(); + const { assetReserves, oTokenReserves } = await getPoolReserves(); // If the asset reserves delta is a function, call it to check the asset token reserves if (typeof delta.reserves.assetToken == "function") { // Call test function to check the asset token reserves - delta.reserves.assetToken(assetTokenReserves); + delta.reserves.assetToken(assetReserves); } else { - expect(assetTokenReserves, "assetToken reserves").to.equal( + expect(assetReserves, "assetToken reserves").to.equal( dataBefore.reserves.assetToken.add(delta.reserves.assetToken) ); } @@ -1258,7 +1259,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (context) => { pool, vaultSigner, assetToken, - } = await context(); + } = context; await vault.connect(nick).mint(assetToken.address, assetDepositAmount, 0); @@ -1284,14 +1285,12 @@ const shouldBehaveLikeAlgebraAmoStrategy = (context) => { ); await logProfit(dataBefore); - console.log("HERE 1"); // Check emitted events await expect(tx).to.emit(amoStrategy, "Deposit") .withArgs(assetToken.address, pool.address, assetDepositAmount); - // await expect(tx).to.emit(amoStrategy, "Deposit") - // .withArgs(oToken.address, pool.address, oTokenMintAmount); + await expect(tx).to.emit(amoStrategy, "Deposit") + .withArgs(oToken.address, pool.address, oTokenMintAmount); - console.log("HERE 2"); // Calculate the value of the asset token and oToken added to the pool if the pool was balanced const depositValue = calcReserveValue({ assetToken: assetDepositAmount, @@ -1307,7 +1306,6 @@ const shouldBehaveLikeAlgebraAmoStrategy = (context) => { gaugeSupply: lpMintAmount, }); - console.log("HERE 3"); expect( await pool.balanceOf(amoStrategy.address), "Strategy's pool LP balance" @@ -1319,7 +1317,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (context) => { } async function assertFailedDeposit(assetDepositAmount, errorMessage) { - const { wS, vaultSigner, amoStrategy } = fixture; + const { wS, vaultSigner, amoStrategy } = context; const dataBefore = await snapData(); await logSnapData(dataBefore, "\nBefore depositing wS to strategy"); @@ -1338,7 +1336,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (context) => { } async function assertFailedDepositAll(assetDepositAmount, errorMessage) { - const { wS, vaultSigner, amoStrategy } = fixture; + const { wS, vaultSigner, amoStrategy } = context; const dataBefore = await snapData(); await logSnapData(dataBefore, "\nBefore depositing all wS to strategy"); @@ -1355,13 +1353,13 @@ const shouldBehaveLikeAlgebraAmoStrategy = (context) => { } async function assertWithdrawAll() { - const { amoStrategy, pool, oSonic, vaultSigner, wS } = - fixture; + const { amoStrategy, pool, oToken, vaultSigner, assetToken } = + context; const dataBefore = await snapData(); await logSnapData(dataBefore); - const { osBurnAmount, wsWithdrawAmount } = await calcWithdrawAllAmounts(); + const { oTokenBurnAmount, assetTokenWithdrawAmount } = await calcWithdrawAllAmounts(); // Now try to withdraw all the wS from the strategy const tx = await amoStrategy.connect(vaultSigner).withdrawAll(); @@ -1372,28 +1370,28 @@ const shouldBehaveLikeAlgebraAmoStrategy = (context) => { // Check emitted events await expect(tx) .to.emit(amoStrategy, "Withdrawal") - .withArgs(assetToken.address, pool.address, wsWithdrawAmount); + .withArgs(assetToken.address, pool.address, assetTokenWithdrawAmount); await expect(tx) .to.emit(amoStrategy, "Withdrawal") - .withArgs(oSonic.address, pool.address, osBurnAmount); + .withArgs(oToken.address, pool.address, oTokenBurnAmount); - // Calculate the value of the wS and OS tokens removed from the pool if the pool was balanced + // Calculate the value of the asset token and oToken removed from the pool if the pool was balanced const withdrawValue = calcReserveValue({ - ws: wsWithdrawAmount, - os: osBurnAmount, + assetToken: assetTokenWithdrawAmount, + oToken: oTokenBurnAmount, }); await assertChangedData(dataBefore, { stratBalance: withdrawValue.mul(-1), - osSupply: osBurnAmount.mul(-1), - reserves: { ws: wsWithdrawAmount.mul(-1), os: osBurnAmount.mul(-1) }, - vaultWSBalance: wsWithdrawAmount, + oTokenSupply: oTokenBurnAmount.mul(-1), + reserves: { assetToken: assetTokenWithdrawAmount.mul(-1), oToken: oTokenBurnAmount.mul(-1) }, + vaultAssetBalance: assetTokenWithdrawAmount, stratGaugeBalance: dataBefore.stratGaugeBalance.mul(-1), }); expect( - wsWithdrawAmount.add(osBurnAmount), - "wS withdraw and OS burnt >= strategy balance" + assetTokenWithdrawAmount.add(oTokenBurnAmount), + "asset token withdraw and oToken burn >= strategy balance" ).to.gte(dataBefore.stratBalance); expect( @@ -1401,70 +1399,70 @@ const shouldBehaveLikeAlgebraAmoStrategy = (context) => { "Strategy's pool LP balance" ).to.equal(0); expect( - await oSonic.balanceOf(amoStrategy.address), - "Strategy's OS balance" + await oToken.balanceOf(amoStrategy.address), + "Strategy's oToken balance" ).to.equal(0); } - async function assertWithdrawPartial(wsWithdrawAmount) { + async function assertWithdrawPartial(assetTokenWithdrawAmount) { const { amoStrategy, - oSonic, + oToken, pool, - oSonicVault, + vault, vaultSigner, - wS, - } = fixture; + assetToken, + } = context; const dataBefore = await snapData(); - const { lpBurnAmount, osBurnAmount } = await calcOSWithdrawAmount( - wsWithdrawAmount + const { lpBurnAmount, oTokenBurnAmount } = await calcOTokenWithdrawAmount( + assetTokenWithdrawAmount ); - // Now try to withdraw the wS from the strategy + // Now try to withdraw the asset token from the strategy const tx = await amoStrategy .connect(vaultSigner) - .withdraw(oSonicVault.address, assetToken.address, wsWithdrawAmount); + .withdraw(vault.address, assetToken.address, assetTokenWithdrawAmount); await logSnapData( await snapData(), - `\nAfter withdraw of ${formatUnits(wsWithdrawAmount)}` + `\nAfter withdraw of ${formatUnits(assetTokenWithdrawAmount)}` ); await logProfit(dataBefore); // Check emitted events await expect(tx) .to.emit(amoStrategy, "Withdrawal") - .withArgs(assetToken.address, pool.address, wsWithdrawAmount); + .withArgs(assetToken.address, pool.address, assetTokenWithdrawAmount); await expect(tx).to.emit(amoStrategy, "Withdrawal").withNamedArgs({ - _asset: oSonic.address, + _asset: oToken.address, _pToken: pool.address, }); - // Calculate the value of the wS and OS tokens removed from the pool if the pool was balanced + // Calculate the value of the asset token and OToken removed from the pool if the pool was balanced const withdrawValue = calcReserveValue({ - ws: wsWithdrawAmount, - os: osBurnAmount, + assetToken: assetTokenWithdrawAmount, + oToken: oTokenBurnAmount, }); await assertChangedData(dataBefore, { stratBalance: withdrawValue.mul(-1), - osSupply: osBurnAmount.mul(-1), + oTokenSupply: oTokenBurnAmount.mul(-1), reserves: { - ws: (actualWsReserve) => { - const expectedWsReserves = - dataBefore.reserves.assetToken.sub(wsWithdrawAmount); - - expect(actualWsReserve).to.withinRange( - expectedWsReserves.sub(50), - expectedWsReserves, - "wS reserves" + assetToken: (actualAssetTokenReserve) => { + const expectedAssetTokenReserves = + dataBefore.reserves.assetToken.sub(assetTokenWithdrawAmount); + + expect(actualAssetTokenReserve).to.withinRange( + expectedAssetTokenReserves.sub(50), + expectedAssetTokenReserves, + "asset token reserves" ); }, - os: osBurnAmount.mul(-1), + oToken: oTokenBurnAmount.mul(-1), }, - vaultWSBalance: wsWithdrawAmount, + vaultAssetBalance: assetTokenWithdrawAmount, gaugeSupply: lpBurnAmount.mul(-1), }); @@ -1473,13 +1471,13 @@ const shouldBehaveLikeAlgebraAmoStrategy = (context) => { "Strategy's pool LP balance" ).to.equal(0); expect( - await oSonic.balanceOf(amoStrategy.address), - "Strategy's OS balance" + await oToken.balanceOf(amoStrategy.address), + "Strategy's OToken balance" ).to.equal(0); } async function assertSwapAssetsToPool(wsAmount) { - const { oSonic, amoStrategy, pool, strategist, wS } = fixture; + const { oSonic, amoStrategy, pool, strategist, wS } = context; const dataBefore = await snapData(); await logSnapData( @@ -1582,7 +1580,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (context) => { // Calculate the minted OS amount for a deposit async function calcOTokenMintAmount(assetDepositAmount) { - const { pool } = await context(); + const { pool } = context; const { assetReserves, oTokenReserves } = await getPoolReserves(); @@ -1596,7 +1594,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (context) => { } async function getPoolReserves() { - const { pool, oTokenPoolIndex } = await context(); + const { pool, oTokenPoolIndex } = context; let assetReserves, oTokenReserves; // Get the reserves of the pool @@ -1607,53 +1605,50 @@ const shouldBehaveLikeAlgebraAmoStrategy = (context) => { return { assetReserves, oTokenReserves }; } - // Calculate the amount of OS burnt from a withdraw - async function calcOSWithdrawAmount(wsWithdrawAmount) { - const { pool } = fixture; + // Calculate the amount of oToken burnt from a withdraw + async function calcOTokenWithdrawAmount(assetTokenWithdrawAmount) { + const { pool } = context; - // Get the reserves of the pool - const { _reserve0: wsReserves, _reserve1: osReserves } = - await pool.getReserves(); + const { assetReserves, oTokenReserves } = await getPoolReserves(); - // lp tokens to burn = wS withdrawn * total LP supply / wS pool balance + // lp tokens to burn = asset token withdrawn * total LP supply / asset token pool balance const totalLpSupply = await pool.totalSupply(); - const lpBurnAmount = wsWithdrawAmount + const lpBurnAmount = assetTokenWithdrawAmount .mul(totalLpSupply) - .div(wsReserves) + .div(assetReserves) .add(1); - // OS to burn = LP tokens to burn * OS reserves / total LP supply - const osBurnAmount = lpBurnAmount.mul(osReserves).div(totalLpSupply); + // OToken to burn = LP tokens to burn * OToken reserves / total LP supply + const oTokenBurnAmount = lpBurnAmount.mul(oTokenReserves).div(totalLpSupply); - log(`OS burn amount : ${formatUnits(osBurnAmount)}`); + log(`OToken burn amount : ${formatUnits(oTokenBurnAmount)}`); - return { lpBurnAmount, osBurnAmount }; + return { lpBurnAmount, oTokenBurnAmount }; } - // Calculate the OS and wS amounts from a withdrawAll + // Calculate the OToken and asset token amounts from a withdrawAll async function calcWithdrawAllAmounts() { - const { amoStrategy, swapXGauge, pool } = fixture; + const { amoStrategy, gauge, pool } = context; // Get the reserves of the pool - const { _reserve0: wsReserves, _reserve1: osReserves } = - await pool.getReserves(); - const strategyLpAmount = await swapXGauge.balanceOf( + const { assetReserves, oTokenReserves } = await getPoolReserves(); + const strategyLpAmount = await gauge.balanceOf( amoStrategy.address ); const totalLpSupply = await pool.totalSupply(); - // wS to withdraw = wS pool balance * strategy LP amount / total pool LP amount - const wsWithdrawAmount = wsReserves + // asset token to withdraw = asset token pool balance * strategy LP amount / total pool LP amount + const assetTokenWithdrawAmount = assetReserves .mul(strategyLpAmount) .div(totalLpSupply); // OS to burn = OS pool balance * strategy LP amount / total pool LP amount - const osBurnAmount = osReserves.mul(strategyLpAmount).div(totalLpSupply); + const oTokenBurnAmount = oTokenReserves.mul(strategyLpAmount).div(totalLpSupply); - log(`wS withdraw amount : ${formatUnits(wsWithdrawAmount)}`); - log(`OS burn amount : ${formatUnits(osBurnAmount)}`); + log(`asset token withdraw amount : ${formatUnits(assetTokenWithdrawAmount)}`); + log(`oToken burn amount : ${formatUnits(oTokenBurnAmount)}`); return { - wsWithdrawAmount, - osBurnAmount, + assetTokenWithdrawAmount, + oTokenBurnAmount, }; } }); diff --git a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js index 000ca84901..17c6fe0be2 100644 --- a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js +++ b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js @@ -8,8 +8,20 @@ describe.only("Sonic Fork Test: Sonic Staking Strategy", function () { const fixture = await swapXAMOFixture(); return { fixture: swapXAMOFixture, - loadFixture: (config = {}) => { - return createFixtureLoader(swapXAMOFixture, config); + loadFixture: ({ + assetMintAmount = 0, + depositToStrategy = false, + balancePool = false, + poolAddAssetAmount = 0, + poolAddOTokenAmount = 0 + } = {}) => { + return createFixtureLoader(swapXAMOFixture, { + wsMintAmount: assetMintAmount, + depositToStrategy, + balancePool, + poolAddwSAmount: poolAddAssetAmount, + poolAddOSAmount: poolAddOTokenAmount + }); }, addresses: addresses.sonic, assetToken: fixture.wS, // address of the asset token in the pool From 154e81cbfd82d752cc1e95069fbd0a696ea5a6a7 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 16 Feb 2026 10:18:41 +0100 Subject: [PATCH 10/44] more work on fork tests --- .../test/behaviour/algebraAmoStrategy.js | 593 +++++++++--------- .../sonic/swapx-amo.sonic.fork-test.js | 2 + 2 files changed, 306 insertions(+), 289 deletions(-) diff --git a/contracts/test/behaviour/algebraAmoStrategy.js b/contracts/test/behaviour/algebraAmoStrategy.js index cafa97bbb0..ddfa8f947a 100644 --- a/contracts/test/behaviour/algebraAmoStrategy.js +++ b/contracts/test/behaviour/algebraAmoStrategy.js @@ -1,6 +1,7 @@ const { expect } = require("chai"); const { formatUnits, parseUnits } = require("ethers/lib/utils"); const { impersonateAndFund } = require("../../utils/signers"); +const { setERC20TokenBalance } = require("../_fund"); const { isCI } = require("../helpers"); const log = require("../../utils/logger")("test:fork:algebra:amo"); @@ -305,20 +306,20 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { nick, amoStrategy, gauge, - swpx, + rewardToken, strategist, } = context; - const swpxBalanceBefore = await swpx.balanceOf(strategist.address); + const rewardTokenBalanceBefore = await rewardToken.balanceOf(strategist.address); // Send some SWPx rewards to the gauge const distributorAddress = await gauge.DISTRIBUTION(); const distributorSigner = await impersonateAndFund(distributorAddress); const rewardAmount = parseUnits("1000"); - await setERC20TokenBalance(distributorAddress, swpx, rewardAmount); + await setERC20TokenBalance(distributorAddress, rewardToken, rewardAmount); await gauge .connect(distributorSigner) - .notifyRewardAmount(swpx.address, rewardAmount); + .notifyRewardAmount(rewardToken.address, rewardAmount); // Harvest the rewards // prettier-ignore @@ -327,32 +328,32 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { await expect(tx).to.emit(amoStrategy, "RewardTokenCollected"); - const swpxBalanceAfter = await swpx.balanceOf(strategist.address); + const rewardTokenBalanceAfter = await rewardToken.balanceOf(strategist.address); log( `Rewards collected ${formatUnits( - swpxBalanceAfter.sub(swpxBalanceBefore) - )} SWPx` + rewardTokenBalanceAfter.sub(rewardTokenBalanceBefore) + )}` ); - expect(swpxBalanceAfter).to.gt(swpxBalanceBefore); + expect(rewardTokenBalanceAfter).to.gt(rewardTokenBalanceBefore); }); it("Attacker front-run deposit within range by adding wS to the pool", async () => { - const { nick, oSonic, vaultSigner, amoStrategy, assetToken } = + const { nick, oToken, vaultSigner, amoStrategy, assetToken } = context; - const attackerWsBalanceBefore = await assetToken.balanceOf(nick.address); - const wsAmountIn = parseUnits("20000"); + const attackerAssetTokenBalanceBefore = await assetToken.balanceOf(nick.address); + const assetTokenAmountIn = parseUnits("20000"); const dataBeforeSwap = await snapData(); logSnapData( dataBeforeSwap, `\nBefore attacker swaps ${formatUnits( - wsAmountIn - )} wS into the pool for OS` + assetTokenAmountIn + )} asset token into the pool for OToken` ); - // Attacker swaps a lot of wS for OS in the pool - // This drops the pool's wS/OS price and increases the OS/wS price - const osAmountOut = await poolSwapTokensIn(assetToken, wsAmountIn); + // Attacker swaps a lot of asset token for OToken in the pool + // This drops the pool's asset token/OToken price and increases the OToken/asset token price + const oTokenAmountOut = await poolSwapTokensIn(assetToken, assetTokenAmountIn); const depositAmount = parseUnits("200000"); @@ -361,7 +362,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { dataBeforeDeposit, `\nAfter attacker tilted pool and before strategist deposits ${formatUnits( depositAmount - )} wS` + )} asset token` ); // Vault deposits wS to the strategy @@ -377,271 +378,274 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { dataAfterDeposit, `\nAfter deposit of ${formatUnits( depositAmount - )} wS to strategy and before attacker swaps ${formatUnits( - osAmountOut - )} OS back into the pool for wS` + )} asset token to strategy and before attacker swaps ${formatUnits( + oTokenAmountOut + )} OToken back into the pool for asset token` ); await logProfit(dataBeforeSwap); // Attacker swaps the OS back for wS - await poolSwapTokensIn(oSonic, osAmountOut); + await poolSwapTokensIn(oToken, oTokenAmountOut); const dataAfterFinalSwap = await snapData(); logSnapData( dataAfterFinalSwap, `\nAfter attacker swaps ${formatUnits( - osAmountOut - )} OS back into the pool for wS` + oTokenAmountOut + )} OToken back into the pool for asset token` ); await logProfit(dataBeforeSwap); const attackerWsBalanceAfter = await assetToken.balanceOf(nick.address); log( `Attacker's profit ${formatUnits( - attackerWsBalanceAfter.sub(attackerWsBalanceBefore) - )} wS` + attackerWsBalanceAfter.sub(attackerAssetTokenBalanceBefore) + )} asset token` ); }); - }); // TODO DELETE THIS LINE AFTER UNCOMMENT BELOW - // describe("When attacker front-run by adding a lot of wS to the pool", () => { - // let attackerWsBalanceBefore; - // let dataBeforeSwap; - // let osAmountOut; - // beforeEach(async () => { - // const { nick, wS } = fixture; - - // attackerWsBalanceBefore = await assetToken.balanceOf(nick.address); - // const wsAmountIn = parseUnits("10000000"); - - // dataBeforeSwap = await snapData(); - // logSnapData( - // dataBeforeSwap, - // `\nBefore attacker swaps ${formatUnits( - // wsAmountIn - // )} wS into the pool for OS` - // ); - - // // Attacker swaps a lot of wS for OS in the pool - // // This drops the pool's wS/OS price and increases the OS/wS price - // osAmountOut = await poolSwapTokensIn(wS, wsAmountIn); - // }); - // it("Strategist fails to deposit to strategy", async () => { - // await assertFailedDeposit(parseUnits("5000"), "price out of range"); - // }); - // it("Strategist fails to deposit all to strategy", async () => { - // await assertFailedDepositAll(parseUnits("5000"), "price out of range"); - // }); - // it("Strategist should withdraw from strategy with a profit", async () => { - // const { - // nick, - // oSonic, - // oSonicVault, - // vaultSigner, - // amoStrategy, - // wS, - // } = fixture; - // const withdrawAmount = parseUnits("4000"); - - // const dataBeforeWithdraw = await snapData(); - // logSnapData( - // dataBeforeWithdraw, - // `\nBefore strategist withdraw ${formatUnits(withdrawAmount)} wS` - // ); - - // const tx = await amoStrategy - // .connect(vaultSigner) - // .withdraw(oSonicVault.address, assetToken.address, withdrawAmount); - - // const dataAfterWithdraw = await snapData(); - // logSnapData( - // dataAfterWithdraw, - // `\nAfter withdraw and before attacker swaps ${formatUnits( - // osAmountOut - // )} OS back into the pool for wS` - // ); - // await logProfit(dataBeforeSwap); - - // // Get how much OS was burnt - // const receipt = await tx.wait(); - // const redeemEvent = receipt.events.find( - // (e) => e.event === "Withdrawal" && e.args._asset === oSonic.address - // ); - // log(`\nWithdraw burnt ${formatUnits(redeemEvent.args._amount)} OS`); - - // // Attacker swaps the OS back for wS - // await poolSwapTokensIn(oSonic, osAmountOut); - - // const dataAfterFinalSwap = await snapData(); - // logSnapData( - // dataAfterFinalSwap, - // "\nAfter attacker swaps OS into the pool for wS" - // ); - // const profit = await logProfit(dataBeforeSwap); - // expect(profit, "vault profit").to.gt(0); - - // const attackerWsBalanceAfter = await assetToken.balanceOf(nick.address); - // log( - // `Attacker's profit/loss ${formatUnits( - // attackerWsBalanceAfter.sub(attackerWsBalanceBefore) - // )} wS` - // ); - // }); - // }); - // describe("When attacker front-run by adding a lot of OS to the pool", () => { - // const attackerBalanceBefore = {}; - // let dataBeforeSwap; - // let wsAmountOut; - // beforeEach(async () => { - // const { nick, oSonic, oSonicVault, wS } = fixture; - - // const osAmountIn = parseUnits("10000000"); - // // Mint OS using wS - // await oSonicVault.connect(nick).mint(assetToken.address, osAmountIn, 0); - - // attackerBalanceBefore.os = await oSonic.balanceOf(nick.address); - // attackerBalanceBefore.ws = await assetToken.balanceOf(nick.address); - - // dataBeforeSwap = await snapData(); - // logSnapData( - // dataBeforeSwap, - // `\nBefore attacker swaps ${formatUnits( - // osAmountIn - // )} OS into the pool for wS` - // ); - - // // Attacker swaps a lot of OS for wS in the pool - // // This increases the pool's wS/OS price and decreases the OS/wS price - // wsAmountOut = await poolSwapTokensIn(oSonic, osAmountIn); - // }); - // it("Strategist fails to deposit to strategy", async () => { - // await assertFailedDeposit(parseUnits("5000"), "price out of range"); - // }); - // it("Strategist fails to deposit all to strategy", async () => { - // await assertFailedDepositAll(parseUnits("5000"), "price out of range"); - // }); - // it("Strategist should withdraw from strategy with a profit", async () => { - // const { - // nick, - // oSonic, - // oSonicVault, - // vaultSigner, - // amoStrategy, - // wS, - // } = fixture; - // const withdrawAmount = parseUnits("200"); - - // const dataBeforeWithdraw = await snapData(); - // logSnapData( - // dataBeforeWithdraw, - // `\nBefore strategist withdraw ${formatUnits(withdrawAmount)} wS` - // ); - - // const tx = await amoStrategy - // .connect(vaultSigner) - // .withdraw(oSonicVault.address, assetToken.address, withdrawAmount); - - // const dataAfterWithdraw = await snapData(); - // logSnapData( - // dataAfterWithdraw, - // `\nAfter withdraw and before attacker swaps ${formatUnits( - // wsAmountOut - // )} wS back into the pool for OS` - // ); - // await logProfit(dataBeforeSwap); - - // // Get how much OS was burnt - // const receipt = await tx.wait(); - // const redeemEvent = receipt.events.find( - // (e) => e.event === "Withdrawal" && e.args._asset === oSonic.address - // ); - // log(`\nWithdraw burnt ${formatUnits(redeemEvent.args._amount)} OS`); - - // // Attacker swaps the wS back for OS - // await poolSwapTokensIn(wS, wsAmountOut); - - // const dataAfterFinalSwap = await snapData(); - // logSnapData( - // dataAfterFinalSwap, - // "\nAfter attacker swaps wS into the pool for OS" - // ); - // const profit = await logProfit(dataBeforeSwap); - // expect(profit, "vault profit").to.gt(0); - - // const attackerBalanceAfter = {}; - // attackerBalanceAfter.os = await oSonic.balanceOf(nick.address); - // attackerBalanceAfter.ws = await assetToken.balanceOf(nick.address); - // log( - // `Attacker's profit/loss ${formatUnits( - // attackerBalanceAfter.os.sub(attackerBalanceBefore.os) - // )} OS and ${formatUnits( - // attackerBalanceAfter.assetToken.sub(attackerBalanceBefore.ws) - // )} wS` - // ); - // }); - // }); - // }); + + describe("When attacker front-run by adding a lot of asset token to the pool", () => { + let attackerAssetBalanceBefore; + let dataBeforeSwap; + let oTokenAmountOut; + beforeEach(async () => { + context = await contextFunction(); + fixture = await context.loadFixture(); + const { nick, assetToken } = context; + + attackerAssetBalanceBefore = await assetToken.balanceOf(nick.address); + const assetTokenAmountIn = parseUnits("10000000"); + + dataBeforeSwap = await snapData(); + logSnapData( + dataBeforeSwap, + `\nBefore attacker swaps ${formatUnits( + assetTokenAmountIn + )} asset token into the pool for OToken` + ); - // describe("with a lot more OS in the pool", () => { - // const loadFixture = createFixtureLoader(swapXAMOFixture, { - // wsMintAmount: 5000, - // depositToStrategy: true, - // balancePool: true, - // poolAddOSAmount: 1000000, - // }); - // beforeEach(async () => { - // fixture = await loadFixture(); - // }); - // it("Vault should fail to deposit wS to AMO strategy", async function () { - // await assertFailedDeposit(parseUnits("5000"), "price out of range"); - // }); - // it("Vault should be able to withdraw all", async () => { - // await assertWithdrawAll(); - // }); - // it("Vault should be able to partially withdraw", async () => { - // await assertWithdrawPartial(parseUnits("4000")); - // }); - // it("Strategist should swap a little assets to the pool", async () => { - // await assertSwapAssetsToPool(parseUnits("3")); - // }); - // it("Strategist should swap enough wS to get the pool close to balanced", async () => { - // const { pool } = fixture; - // const { _reserve0: wsReserves, _reserve1: osReserves } = - // await pool.getReserves(); - // // 5% of the extra OS - // const osAmount = osReserves.sub(wsReserves).mul(5).div(100); - // const wsAmount = osAmount.mul(wsReserves).div(osReserves); - // log(`OS amount: ${formatUnits(osAmount)}`); - // log(`wS amount: ${formatUnits(wsAmount)}`); + // Attacker swaps a lot of asset token for OToken in the pool + // This drops the pool's asset token/OToken price and increases the OToken/asset token price + oTokenAmountOut = await poolSwapTokensIn(assetToken, assetTokenAmountIn); + }); + it("Strategist fails to deposit to strategy", async () => { + await assertFailedDeposit(parseUnits("5000"), "price out of range"); + }); + it("Strategist fails to deposit all to strategy", async () => { + await assertFailedDepositAll(parseUnits("5000"), "price out of range"); + }); + it("Strategist should withdraw from strategy with a profit", async () => { + const { + nick, + oToken, + vault, + vaultSigner, + amoStrategy, + assetToken, + } = context; + const withdrawAmount = parseUnits("4000"); + + const dataBeforeWithdraw = await snapData(); + logSnapData( + dataBeforeWithdraw, + `\nBefore strategist withdraw ${formatUnits(withdrawAmount)} asset token` + ); - // await assertSwapAssetsToPool(wsAmount); - // }); - // it("Strategist should swap a lot of assets to the pool", async () => { - // await assertSwapAssetsToPool(parseUnits("3000")); - // }); - // it("Strategist should swap most of the wS owned by the strategy", async () => { - // // TODO calculate how much wS should be swapped to get the pool balanced - // await assertSwapAssetsToPool(parseUnits("4400")); - // }); - // it("Strategist should fail to add more wS than owned by the strategy", async () => { - // const { amoStrategy, strategist } = fixture; + const tx = await amoStrategy + .connect(vaultSigner) + .withdraw(vault.address, assetToken.address, withdrawAmount); - // const tx = amoStrategy - // .connect(strategist) - // .swapAssetsToPool(parseUnits("2000000")); + const dataAfterWithdraw = await snapData(); + logSnapData( + dataAfterWithdraw, + `\nAfter withdraw and before attacker swaps ${formatUnits( + oTokenAmountOut + )} OToken back into the pool for asset token` + ); + await logProfit(dataBeforeSwap); - // await expect(tx).to.be.revertedWith("Not enough LP tokens in gauge"); - // }); - // it("Strategist should fail to add more OS to the pool", async () => { - // const { amoStrategy, strategist } = fixture; + // Get how much OS was burnt + const receipt = await tx.wait(); + const redeemEvent = receipt.events.find( + (e) => e.event === "Withdrawal" && e.args._asset === oToken.address + ); + log(`\nWithdraw burnt ${formatUnits(redeemEvent.args._amount)} OS`); - // // try swapping OS into the pool - // const tx = amoStrategy - // .connect(strategist) - // .swapOTokensToPool(parseUnits("0.001")); + // Attacker swaps the OS back for wS + await poolSwapTokensIn(oToken, oTokenAmountOut); - // await expect(tx).to.be.revertedWith("OTokens balance worse"); - // }); - // }); + const dataAfterFinalSwap = await snapData(); + logSnapData( + dataAfterFinalSwap, + "\nAfter attacker swaps OToken into the pool for the asset token" + ); + const profit = await logProfit(dataBeforeSwap); + expect(profit, "vault profit").to.gt(0); + + const attackerWsBalanceAfter = await assetToken.balanceOf(nick.address); + log( + `Attacker's profit/loss ${formatUnits( + attackerWsBalanceAfter.sub(attackerAssetBalanceBefore) + )} wS` + ); + }); + }); + describe("When attacker front-run by adding a lot of OToken to the pool", () => { + const attackerBalanceBefore = {}; + let dataBeforeSwap; + let assetAmountOut; + beforeEach(async () => { + context = await contextFunction(); + fixture = await context.loadFixture(); + const { nick, oToken, vault, assetToken } = context; + + const oTokenAmountIn = parseUnits("10000000"); + + // Mint OToken using asset token + await vault.connect(nick).mint(assetToken.address, oTokenAmountIn, 0); + + attackerBalanceBefore.oToken = await oToken.balanceOf(nick.address); + attackerBalanceBefore.assetToken = await assetToken.balanceOf(nick.address); + + dataBeforeSwap = await snapData(); + logSnapData( + dataBeforeSwap, + `\nBefore attacker swaps ${formatUnits( + oTokenAmountIn + )} OToken into the pool for asset token` + ); + + // Attacker swaps a lot of OToken for the asset token in the pool + // This increases the pool's asset/OToken price and decreases the OToken/asset price + assetAmountOut = await poolSwapTokensIn(oToken, oTokenAmountIn); + }); + it("Strategist fails to deposit to strategy", async () => { + await assertFailedDeposit(parseUnits("5000"), "price out of range"); + }); + it("Strategist fails to deposit all to strategy", async () => { + await assertFailedDepositAll(parseUnits("5000"), "price out of range"); + }); + it("Strategist should withdraw from strategy with a profit", async () => { + const { + nick, + oToken, + vault, + vaultSigner, + amoStrategy, + assetToken, + } = context; + const withdrawAmount = parseUnits("200"); + + const dataBeforeWithdraw = await snapData(); + logSnapData( + dataBeforeWithdraw, + `\nBefore strategist withdraw ${formatUnits(withdrawAmount)} asset token` + ); + + const tx = await amoStrategy + .connect(vaultSigner) + .withdraw(vault.address, assetToken.address, withdrawAmount); + + const dataAfterWithdraw = await snapData(); + logSnapData( + dataAfterWithdraw, + `\nAfter withdraw and before attacker swaps ${formatUnits( + assetAmountOut + )} asset token back into the pool for OToken` + ); + await logProfit(dataBeforeSwap); + + // Get how much OS was burnt + const receipt = await tx.wait(); + const redeemEvent = receipt.events.find( + (e) => e.event === "Withdrawal" && e.args._asset === oToken.address + ); + log(`\nWithdraw burnt ${formatUnits(redeemEvent.args._amount)} OS`); + + // Attacker swaps the asset token back for OToken + await poolSwapTokensIn(assetToken, assetAmountOut); + + const dataAfterFinalSwap = await snapData(); + logSnapData( + dataAfterFinalSwap, + "\nAfter attacker swaps asset token into the pool for OToken" + ); + const profit = await logProfit(dataBeforeSwap); + expect(profit, "vault profit").to.gt(0); + + const attackerBalanceAfter = {}; + attackerBalanceAfter.oToken = await oToken.balanceOf(nick.address); + attackerBalanceAfter.assetToken = await assetToken.balanceOf(nick.address); + log( + `Attacker's profit/loss ${formatUnits( + attackerBalanceAfter.oToken.sub(attackerBalanceBefore.oToken) + )} OToken and ${formatUnits( + attackerBalanceAfter.assetToken.sub(attackerBalanceBefore.assetToken) + )} asset token` + ); + }); + }); + }); + + describe.only("with a lot more OToken in the pool", () => { + beforeEach(async () => { + context = await contextFunction(); + fixture = await context.loadFixture({ + assetMintAmount: 5000, + depositToStrategy: true, + balancePool: true, + poolAddOTokenAmount: 1000000, + }); + }); + it("Vault should fail to deposit asset token to AMO strategy", async function () { + await assertFailedDeposit(parseUnits("5000"), "price out of range"); + }); + it("Vault should be able to withdraw all", async () => { + await assertWithdrawAll(); + }); + it("Vault should be able to partially withdraw", async () => { + await assertWithdrawPartial(parseUnits("4000")); + }); + it("Strategist should swap a little assets to the pool", async () => { + await assertSwapAssetsToPool(parseUnits("3")); + }); + it("Strategist should swap enough asset token to get the pool close to balanced", async () => { + const { assetReserves, oTokenReserves } = await getPoolReserves(); + // 5% of the extra oToken in the pool + const oTokenAmount = oTokenReserves.sub(assetReserves).mul(5).div(100); + const assetAmount = oTokenAmount.mul(assetReserves).div(oTokenReserves); + log(`oToken amount: ${formatUnits(oTokenAmount)}`); + log(`asset amount: ${formatUnits(assetAmount)}`); + + await assertSwapAssetsToPool(assetAmount); + }); + it("Strategist should swap a lot of assets to the pool", async () => { + await assertSwapAssetsToPool(parseUnits("3000")); + }); + it("Strategist should swap most of the wS owned by the strategy", async () => { + // TODO calculate how much wS should be swapped to get the pool balanced + await assertSwapAssetsToPool(parseUnits("4400")); + }); + it("Strategist should fail to add more wS than owned by the strategy", async () => { + const { amoStrategy, strategist } = context; + + const tx = amoStrategy + .connect(strategist) + .swapAssetsToPool(parseUnits("2000000")); + + await expect(tx).to.be.revertedWith("Not enough LP tokens in gauge"); + }); + it("Strategist should fail to add more OS to the pool", async () => { + const { amoStrategy, strategist } = context; + + // try swapping OS into the pool + const tx = amoStrategy + .connect(strategist) + .swapOTokensToPool(parseUnits("0.001")); + + await expect(tx).to.be.revertedWith("OTokens balance worse"); + }); + }); // describe("with a little more OS in the pool", () => { // const loadFixture = createFixtureLoader(swapXAMOFixture, { @@ -986,13 +990,24 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { // }); const poolSwapTokensIn = async (tokenIn, amountIn) => { - const { nick, pool, wS } = fixture; + const { nick, pool, assetToken, oTokenPoolIndex } = context; const amountOut = await pool.getAmountOut(amountIn, tokenIn.address); await tokenIn.connect(nick).transfer(pool.address, amountIn); + if (tokenIn.address == assetToken.address) { - await pool.swap(0, amountOut, nick.address, "0x"); + await pool.swap( + oTokenPoolIndex == 1 ? 0 : amountOut, + oTokenPoolIndex == 1 ? amountOut: 0, + nick.address, + "0x" + ); } else { - await pool.swap(amountOut, 0, nick.address, "0x"); + await pool.swap( + oTokenPoolIndex == 0 ? 0 : amountOut, + oTokenPoolIndex == 0 ? amountOut: 0, + nick.address, + "0x" + ); } return amountOut; @@ -1317,13 +1332,13 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { } async function assertFailedDeposit(assetDepositAmount, errorMessage) { - const { wS, vaultSigner, amoStrategy } = context; + const { assetToken, vaultSigner, amoStrategy } = context; const dataBefore = await snapData(); - await logSnapData(dataBefore, "\nBefore depositing wS to strategy"); + await logSnapData(dataBefore, "\nBefore depositing asset token to strategy"); // Vault transfers wS to strategy - await wS + await assetToken .connect(vaultSigner) .transfer(amoStrategy.address, assetDepositAmount); @@ -1336,13 +1351,13 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { } async function assertFailedDepositAll(assetDepositAmount, errorMessage) { - const { wS, vaultSigner, amoStrategy } = context; + const { assetToken, vaultSigner, amoStrategy } = context; const dataBefore = await snapData(); await logSnapData(dataBefore, "\nBefore depositing all wS to strategy"); // Vault transfers wS to strategy - await wS + await assetToken .connect(vaultSigner) .transfer(amoStrategy.address, assetDepositAmount); @@ -1476,45 +1491,45 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { ).to.equal(0); } - async function assertSwapAssetsToPool(wsAmount) { - const { oSonic, amoStrategy, pool, strategist, wS } = context; + async function assertSwapAssetsToPool(assetAmount) { + const { oToken, amoStrategy, pool, strategist, assetToken } = context; const dataBefore = await snapData(); await logSnapData( dataBefore, - `Before swapping ${formatUnits(wsAmount)} wS into the pool` + `Before swapping ${formatUnits(assetAmount)} wS into the pool` ); - const { lpBurnAmount: expectedLpBurnAmount, osBurnAmount: osBurnAmount1 } = - await calcOSWithdrawAmount(wsAmount); + const { lpBurnAmount: expectedLpBurnAmount, oTokenBurnAmount: oTokenBurnAmount1 } = + await calcOTokenWithdrawAmount(assetAmount); // TODO this is not accurate as the liquidity needs to be removed first - const osBurnAmount2 = await pool.getAmountOut(wsAmount, assetToken.address); - const osBurnAmount = osBurnAmount1.add(osBurnAmount2); + const oTokenBurnAmount2 = await pool.getAmountOut(assetAmount, assetToken.address); + const oTokenBurnAmount = oTokenBurnAmount1.add(oTokenBurnAmount2); - // Swap wS to the pool and burn the received OS from the pool + // Swap asset token to the pool and burn the received OToken from the pool const tx = await amoStrategy .connect(strategist) - .swapAssetsToPool(wsAmount); + .swapAssetsToPool(assetAmount); await logSnapData(await snapData(), "\nAfter swapping assets to the pool"); await logProfit(dataBefore); // Check emitted event await expect(tx).to.emittedEvent("SwapAssetsToPool", [ - (actualWsAmount) => { - expect(actualWsAmount).to.withinRange( - wsAmount.sub(1), - wsAmount.add(1), - "SwapAssetsToPool event wsAmount" + (actualAssetTokenAmount) => { + expect(actualAssetTokenAmount).to.withinRange( + assetAmount.sub(1), + assetAmount.add(1), + "SwapAssetsToPool event asset token amount" ); }, expectedLpBurnAmount, - (actualOsBurnAmount) => { - // TODO this can be tightened once osBurnAmount is more accurately calculated - expect(actualOsBurnAmount).to.approxEqualTolerance( - osBurnAmount, + (actualOTokenBurnAmount) => { + // TODO this can be tightened once oTokenBurnAmount is more accurately calculated + expect(actualOTokenBurnAmount).to.approxEqualTolerance( + oTokenBurnAmount, 10, - "SwapAssetsToPool event osBurnt" + "SwapAssetsToPool event oTokenBurnt" ); }, ]); @@ -1523,7 +1538,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { dataBefore, { // stratBalance: osBurnAmount.mul(-1), - vaultWSBalance: 0, + vaultAssetBalance: 0, stratGaugeBalance: 0, }, fixture @@ -1534,8 +1549,8 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { "Strategy's pool LP balance" ).to.equal(0); expect( - await oSonic.balanceOf(amoStrategy.address), - "Strategy's OS balance" + await oToken.balanceOf(amoStrategy.address), + "Strategy's oToken balance" ).to.equal(0); } diff --git a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js index 17c6fe0be2..d85224bf13 100644 --- a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js +++ b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js @@ -26,6 +26,7 @@ describe.only("Sonic Fork Test: Sonic Staking Strategy", function () { addresses: addresses.sonic, assetToken: fixture.wS, // address of the asset token in the pool oToken: fixture.oSonic, // address of the oToken in the pool + rewardToken: fixture.swpx, // address of the reward token amoStrategy: fixture.swapXAMOStrategy, // address of the strategy pool: fixture.swapXPool, gauge: fixture.swapXGauge, // address of the gauge @@ -36,6 +37,7 @@ describe.only("Sonic Fork Test: Sonic Staking Strategy", function () { oTokenPoolIndex: 1, // index of the oToken in the pool vaultSigner: fixture.oSonicVaultSigner, // address of the vault signer vault: fixture.oSonicVault, // address of the vault + harvester: fixture.harvester, // address of the harvester }; }); }); \ No newline at end of file From f64f5e01f599989fddf47f85d14556a0817b01be Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 16 Feb 2026 12:48:31 +0100 Subject: [PATCH 11/44] fix the fork tests --- .../test/behaviour/algebraAmoStrategy.js | 789 +++++++++--------- .../sonic/swapx-amo.sonic.fork-test.js | 47 +- 2 files changed, 418 insertions(+), 418 deletions(-) diff --git a/contracts/test/behaviour/algebraAmoStrategy.js b/contracts/test/behaviour/algebraAmoStrategy.js index ddfa8f947a..0fba8ba5bb 100644 --- a/contracts/test/behaviour/algebraAmoStrategy.js +++ b/contracts/test/behaviour/algebraAmoStrategy.js @@ -1,6 +1,7 @@ const { expect } = require("chai"); const { formatUnits, parseUnits } = require("ethers/lib/utils"); const { impersonateAndFund } = require("../../utils/signers"); +const addresses = require("../../utils/addresses"); const { setERC20TokenBalance } = require("../_fund"); const { isCI } = require("../helpers"); @@ -34,7 +35,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { fixture = await context.loadFixture(); }); it("Should have constants and immutables set", async () => { - const { amoStrategy, assetToken, oToken, pool, gauge, governor } = context; + const { amoStrategy, assetToken, oToken, pool, gauge, governor } = fixture; expect(await amoStrategy.SOLVENCY_THRESHOLD()).to.equal( parseUnits("0.998", 18) @@ -56,7 +57,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { expect(await amoStrategy.maxDepeg()).to.equal(parseUnits("0.01")); }); it("Should be able to check balance", async () => { - const { assetToken, nick, amoStrategy } = context; + const { assetToken, nick, amoStrategy } = fixture; const balance = await amoStrategy.checkBalance(assetToken.address); log(`check balance ${balance}`); @@ -76,7 +77,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { vaultSigner, amoStrategy, pool, - } = context; + } = fixture; expect(await amoStrategy.connect(timelock).isGovernor()).to.equal( true @@ -100,7 +101,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { nick, vaultSigner, amoStrategy, - } = context; + } = fixture; expect(await amoStrategy.connect(timelock).isGovernor()).to.equal( true @@ -144,7 +145,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { timelock, nick, assetToken, - } = context; + } = fixture; const depositAmount = parseUnits("50"); await assetToken @@ -159,7 +160,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { await expect(tx).to.revertedWith("Caller is not the Vault"); } }); - it("Only vault can deposit all wS to AMO strategy", async function () { + it("Only vault can deposit all asset tokens to AMO strategy", async function () { const { amoStrategy, pool, @@ -168,7 +169,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { timelock, nick, assetToken, - } = context; + } = fixture; const depositAmount = parseUnits("50"); await assetToken @@ -204,7 +205,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { await assertWithdrawAll(); }); it("Vault should be able to withdraw all in SwapX Emergency", async () => { - const { amoStrategy, gauge, vaultSigner } = context; + const { amoStrategy, gauge, vaultSigner } = fixture; const gaugeOwner = await gauge.owner(); const ownerSigner = await impersonateAndFund(gaugeOwner); @@ -215,7 +216,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { await amoStrategy.connect(vaultSigner).withdrawAll(); }); it("Should fail to deposit zero asset token", async () => { - const { amoStrategy, vaultSigner, assetToken } = context; + const { amoStrategy, vaultSigner, assetToken } = fixture; const tx = amoStrategy .connect(vaultSigner) @@ -224,7 +225,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { await expect(tx).to.be.revertedWith("Must deposit something"); }); it("Should fail to deposit oToken", async () => { - const { amoStrategy, vaultSigner, oToken } = context; + const { amoStrategy, vaultSigner, oToken } = fixture; const tx = amoStrategy .connect(vaultSigner) @@ -233,7 +234,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { await expect(tx).to.be.revertedWith("Unsupported asset"); }); it("Should fail to withdraw zero asset token", async () => { - const { amoStrategy, vaultSigner, vault, assetToken } = context; + const { amoStrategy, vaultSigner, vault, assetToken } = fixture; const tx = amoStrategy .connect(vaultSigner) @@ -243,7 +244,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { }); it("Should fail to withdraw oToken", async () => { const { amoStrategy, vaultSigner, oToken, vault } = - context; + fixture; const tx = amoStrategy .connect(vaultSigner) @@ -252,7 +253,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { await expect(tx).to.be.revertedWith("Unsupported asset"); }); it("Should fail to withdraw to a user", async () => { - const { amoStrategy, vaultSigner, assetToken, nick } = context; + const { amoStrategy, vaultSigner, assetToken, nick } = fixture; const tx = amoStrategy .connect(vaultSigner) @@ -261,7 +262,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { await expect(tx).to.be.revertedWith("Only withdraw to vault allowed"); }); it("Vault should be able to withdraw all from empty strategy", async () => { - const { amoStrategy, vaultSigner } = context; + const { amoStrategy, vaultSigner } = fixture; await assertWithdrawAll(); // Now try again after all the assets have already been withdrawn @@ -277,7 +278,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { }); it("Only vault can withdraw asset token from AMO strategy", async function () { const { amoStrategy, vault, strategist, timelock, nick, assetToken } = - context; + fixture; for (const signer of [strategist, timelock, nick]) { const tx = amoStrategy @@ -287,8 +288,8 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { await expect(tx).to.revertedWith("Caller is not the Vault"); } }); - it("Only vault and governor can withdraw all WETH from AMO strategy", async function () { - const { amoStrategy, strategist, timelock, nick } = context; + it("Only vault and governor can withdraw all from AMO strategy", async function () { + const { amoStrategy, strategist, timelock, nick } = fixture; for (const signer of [strategist, nick]) { const tx = amoStrategy.connect(signer).withdrawAll(); @@ -308,7 +309,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { gauge, rewardToken, strategist, - } = context; + } = fixture; const rewardTokenBalanceBefore = await rewardToken.balanceOf(strategist.address); @@ -336,9 +337,9 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { ); expect(rewardTokenBalanceAfter).to.gt(rewardTokenBalanceBefore); }); - it("Attacker front-run deposit within range by adding wS to the pool", async () => { + it("Attacker front-run deposit within range by adding asset token to the pool", async () => { const { nick, oToken, vaultSigner, amoStrategy, assetToken } = - context; + fixture; const attackerAssetTokenBalanceBefore = await assetToken.balanceOf(nick.address); const assetTokenAmountIn = parseUnits("20000"); @@ -411,7 +412,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { beforeEach(async () => { context = await contextFunction(); fixture = await context.loadFixture(); - const { nick, assetToken } = context; + const { nick, assetToken } = fixture; attackerAssetBalanceBefore = await assetToken.balanceOf(nick.address); const assetTokenAmountIn = parseUnits("10000000"); @@ -442,7 +443,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { vaultSigner, amoStrategy, assetToken, - } = context; + } = fixture; const withdrawAmount = parseUnits("4000"); const dataBeforeWithdraw = await snapData(); @@ -497,7 +498,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { beforeEach(async () => { context = await contextFunction(); fixture = await context.loadFixture(); - const { nick, oToken, vault, assetToken } = context; + const { nick, oToken, vault, assetToken } = fixture; const oTokenAmountIn = parseUnits("10000000"); @@ -533,7 +534,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { vaultSigner, amoStrategy, assetToken, - } = context; + } = fixture; const withdrawAmount = parseUnits("200"); const dataBeforeWithdraw = await snapData(); @@ -587,7 +588,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { }); }); - describe.only("with a lot more OToken in the pool", () => { + describe("with a lot more OToken in the pool", () => { beforeEach(async () => { context = await contextFunction(); fixture = await context.loadFixture({ @@ -622,12 +623,12 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { it("Strategist should swap a lot of assets to the pool", async () => { await assertSwapAssetsToPool(parseUnits("3000")); }); - it("Strategist should swap most of the wS owned by the strategy", async () => { - // TODO calculate how much wS should be swapped to get the pool balanced + it("Strategist should swap most of the asset token owned by the strategy", async () => { + // TODO calculate how much asset token should be swapped to get the pool balanced await assertSwapAssetsToPool(parseUnits("4400")); }); - it("Strategist should fail to add more wS than owned by the strategy", async () => { - const { amoStrategy, strategist } = context; + it("Strategist should fail to add more asset token than owned by the strategy", async () => { + const { amoStrategy, strategist } = fixture; const tx = amoStrategy .connect(strategist) @@ -635,10 +636,10 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { await expect(tx).to.be.revertedWith("Not enough LP tokens in gauge"); }); - it("Strategist should fail to add more OS to the pool", async () => { - const { amoStrategy, strategist } = context; + it("Strategist should fail to add more OToken to the pool", async () => { + const { amoStrategy, strategist } = fixture; - // try swapping OS into the pool + // Try swapping OToken into the pool. const tx = amoStrategy .connect(strategist) .swapOTokensToPool(parseUnits("0.001")); @@ -647,350 +648,343 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { }); }); - // describe("with a little more OS in the pool", () => { - // const loadFixture = createFixtureLoader(swapXAMOFixture, { - // wsMintAmount: 20000, - // depositToStrategy: true, - // balancePool: true, - // poolAddOSAmount: 5000, - // }); - // beforeEach(async () => { - // fixture = await loadFixture(); - // }); - // it("Vault should deposit wS to AMO strategy", async function () { - // await assertDeposit(parseUnits("12000")); - // }); - // it("Vault should be able to withdraw all", async () => { - // await assertWithdrawAll(); - // }); - // it("Vault should be able to partially withdraw", async () => { - // await assertWithdrawPartial(parseUnits("1000")); - // }); - // it("Strategist should swap a little assets to the pool", async () => { - // await assertSwapAssetsToPool(parseUnits("3")); - // }); - // it("Strategist should swap enough wS to get the pool close to balanced", async () => { - // const { pool } = fixture; - // const { _reserve0: wsReserves, _reserve1: osReserves } = - // await pool.getReserves(); - // // 50% of the extra OS in the pool gets close to balanced - // const osAmount = osReserves.sub(wsReserves).mul(50).div(100); - // const wsAmount = osAmount.mul(wsReserves).div(osReserves); - - // await assertSwapAssetsToPool(wsAmount); - // }); - // it("Strategist should fail to add too much wS to the pool", async () => { - // const { amoStrategy, strategist } = fixture; - - // const dataBefore = await snapData(); - // await logSnapData(dataBefore, "Before swapping assets to the pool"); - - // // try the extra OS in the pool - // const tx = amoStrategy - // .connect(strategist) - // .swapAssetsToPool(parseUnits("5000")); - - // await expect(tx).to.be.revertedWith("Assets overshot peg"); - // }); - // it("Strategist should fail to add zero wS to the pool", async () => { - // const { amoStrategy, strategist } = fixture; - - // const tx = amoStrategy.connect(strategist).swapAssetsToPool(0); - - // await expect(tx).to.be.revertedWith("Must swap something"); - // }); - // it("Strategist should fail to add more OS to the pool", async () => { - // const { amoStrategy, strategist } = fixture; - - // // try swapping OS into the pool - // const tx = amoStrategy - // .connect(strategist) - // .swapOTokensToPool(parseUnits("0.001")); - - // await expect(tx).to.be.revertedWith("OTokens balance worse"); - // }); - // }); - - // describe("with a lot more wS in the pool", () => { - // const loadFixture = createFixtureLoader(swapXAMOFixture, { - // wsMintAmount: 5000, - // depositToStrategy: true, - // balancePool: true, - // poolAddwSAmount: 2000000, - // }); - // beforeEach(async () => { - // fixture = await loadFixture(); - // }); - // it("Vault should fail to deposit wS to strategy", async function () { - // await assertFailedDeposit(parseUnits("6000"), "price out of range"); - // }); - // it("Vault should be able to withdraw all", async () => { - // await assertWithdrawAll(); - // }); - // it("Vault should be able to partially withdraw", async () => { - // await assertWithdrawPartial(parseUnits("1000")); - // }); - // it("Strategist should swap a little OS to the pool", async () => { - // const osAmount = parseUnits("0.3"); - // await assertSwapOTokensToPool(osAmount, fixture); - // }); - // it("Strategist should swap a lot of OS to the pool", async () => { - // const osAmount = parseUnits("5000"); - // await assertSwapOTokensToPool(osAmount, fixture); - // }); - // it("Strategist should get the pool close to balanced", async () => { - // const { pool } = fixture; - // const { _reserve0: wsReserves, _reserve1: osReserves } = - // await pool.getReserves(); - // // 32% of the extra wS in the pool gets pretty close to balanced - // const osAmount = wsReserves.sub(osReserves).mul(32).div(100); - - // await assertSwapOTokensToPool(osAmount); - // }); - // it("Strategist should fail to add so much OS that is overshoots", async () => { - // const { amoStrategy, strategist } = fixture; - - // // try swapping wS into the pool - // const tx = amoStrategy - // .connect(strategist) - // .swapOTokensToPool(parseUnits("999990")); - - // await expect(tx).to.be.revertedWith("OTokens overshot peg"); - // }); - // it("Strategist should fail to add more wS to the pool", async () => { - // const { amoStrategy, strategist } = fixture; - - // // try swapping wS into the pool - // const tx = amoStrategy - // .connect(strategist) - // .swapAssetsToPool(parseUnits("0.0001")); - - // await expect(tx).to.be.revertedWith("Assets balance worse"); - // }); - // }); - - // describe("with a little more wS in the pool", () => { - // const loadFixture = createFixtureLoader(swapXAMOFixture, { - // wsMintAmount: 20000, - // depositToStrategy: true, - // balancePool: true, - // poolAddwSAmount: 20000, - // }); - // beforeEach(async () => { - // fixture = await loadFixture(); - // }); - // it("Vault should deposit wS to AMO strategy", async function () { - // await assertDeposit(parseUnits("18000")); - // }); - // it("Vault should be able to withdraw all", async () => { - // await assertWithdrawAll(); - // }); - // it("Vault should be able to partially withdraw", async () => { - // await assertWithdrawPartial(parseUnits("1000")); - // }); - // it("Strategist should swap a little OS to the pool", async () => { - // const osAmount = parseUnits("8"); - // await assertSwapOTokensToPool(osAmount, fixture); - // }); - // it("Strategist should get the pool close to balanced", async () => { - // const { pool } = fixture; - - // const { _reserve0: wsReserves, _reserve1: osReserves } = - // await pool.getReserves(); - // // 50% of the extra wS in the pool gets pretty close to balanced - // const osAmount = wsReserves.sub(osReserves).mul(50).div(100); - - // await assertSwapOTokensToPool(osAmount, fixture); - // }); - // it("Strategist should fail to add zero OS to the pool", async () => { - // const { amoStrategy, strategist } = fixture; - - // const tx = amoStrategy.connect(strategist).swapOTokensToPool(0); - - // await expect(tx).to.be.revertedWith("Must swap something"); - // }); - // it("Strategist should fail to add too much OS to the pool", async () => { - // const { amoStrategy, strategist } = fixture; - - // // Add OS to the pool - // const tx = amoStrategy - // .connect(strategist) - // .swapOTokensToPool(parseUnits("11000")); - - // await expect(tx).to.be.revertedWith("OTokens overshot peg"); - // }); - // it("Strategist should fail to add more wS to the pool", async () => { - // const { amoStrategy, strategist } = fixture; - - // // try swapping wS into the pool - // const tx = amoStrategy - // .connect(strategist) - // .swapAssetsToPool(parseUnits("0.0001")); - - // await expect(tx).to.be.revertedWith("Assets balance worse"); - // }); - // }); - - // describe("with the strategy owning a small percentage of the pool", () => { - // const loadFixture = createFixtureLoader(swapXAMOFixture, { - // wsMintAmount: 5000, - // depositToStrategy: true, - // balancePool: true, - // }); - // let dataBefore; - // beforeEach(async () => { - // fixture = await loadFixture(); - - // const { nick, wS, oSonic, oSonicVault, pool } = fixture; - - // // Other users adds a lot more liquidity to the pool - // const bigAmount = parseUnits("1000000"); - // // transfer wS to the pool - // await assetToken.connect(nick).transfer(pool.address, bigAmount); - // // Mint OS with wS - // await oSonicVault.connect(nick).mint(assetToken.address, bigAmount.mul(5), 0); - // // transfer OS to the pool - // await oSonic.connect(nick).transfer(pool.address, bigAmount); - // // mint pool LP tokens - // await pool.connect(nick).mint(nick.address); - - // dataBefore = await snapData(); - // await logSnapData(dataBefore); - // }); - // it("a lot of OS is swapped into the pool", async () => { - // const { oSonic, amoStrategy, wS } = fixture; - - // // Swap OS into the pool and wS out - // await poolSwapTokensIn(oSonic, parseUnits("1005000")); - - // await logSnapData(await snapData(), "\nAfter swapping OS into the pool"); - - // // Assert the strategy's balance - // expect( - // await amoStrategy.checkBalance(assetToken.address), - // "Strategy's check balance" - // ).to.withinRange(dataBefore.stratBalance, dataBefore.stratBalance.add(1)); - - // // Swap wS into the pool and OS out - // await poolSwapTokensIn(wS, parseUnits("2000000")); - - // await logSnapData(await snapData(), "\nAfter swapping wS into the pool"); - - // // Assert the strategy's balance - // expect( - // await amoStrategy.checkBalance(assetToken.address), - // "Strategy's check balance" - // ).to.withinRange(dataBefore.stratBalance, dataBefore.stratBalance.add(1)); - // }); - // it("a lot of wS is swapped into the pool", async () => { - // const { amoStrategy, oSonic, wS } = fixture; - - // // Swap wS into the pool and OS out - // await poolSwapTokensIn(wS, parseUnits("1006000")); - - // await logSnapData(await snapData(), "\nAfter swapping wS into the pool"); - - // // Assert the strategy's balance - // expect( - // await amoStrategy.checkBalance(assetToken.address), - // "Strategy's check balance" - // ).to.withinRange(dataBefore.stratBalance, dataBefore.stratBalance.add(1)); - - // // Swap OS into the pool and wS out - // await poolSwapTokensIn(oSonic, parseUnits("1005000")); - - // await logSnapData(await snapData(), "\nAfter swapping OS into the pool"); - - // // Assert the strategy's balance - // expect( - // await amoStrategy.checkBalance(assetToken.address), - // "Strategy's check balance" - // ).to.withinRange(dataBefore.stratBalance, dataBefore.stratBalance.add(1)); - // }); - // }); - - // describe("with an insolvent vault", () => { - // const loadFixture = createFixtureLoader(swapXAMOFixture, { - // wsMintAmount: 5000000, - // depositToStrategy: false, - // }); - // beforeEach(async () => { - // fixture = await loadFixture(); - - // const { oSonicVault, vaultSigner, amoStrategy, wS } = fixture; - - // // Deposit a little to the strategy - // const littleAmount = parseUnits("100"); - // await wS - // .connect(vaultSigner) - // .transfer(amoStrategy.address, littleAmount); - // await amoStrategy - // .connect(vaultSigner) - // .deposit(assetToken.address, littleAmount); - - // const totalAssets = await oSonicVault.totalValue(); - // // Calculate a 0.21% (21 basis points) loss - // const lossAmount = totalAssets.mul(21).div(10000); - // await assetToken.connect(vaultSigner).transfer(addresses.dead, lossAmount); - // expect( - // await assetToken.balanceOf(oSonicVault.address), - // "Must have enough wS in vault to make insolvent" - // ).to.gte(lossAmount); - // }); - // it("Should fail to deposit", async () => { - // const { vaultSigner, amoStrategy, wS } = fixture; - - // // Vault calls deposit on the strategy - // const depositAmount = parseUnits("10"); - // await wS - // .connect(vaultSigner) - // .transfer(amoStrategy.address, depositAmount); - // const tx = amoStrategy - // .connect(vaultSigner) - // .deposit(assetToken.address, depositAmount); - - // await expect(tx).to.be.revertedWith("Protocol insolvent"); - // }); - // it("Should fail to withdraw", async () => { - // const { oSonicVault, vaultSigner, amoStrategy, wS } = fixture; - - // // Vault withdraws from the strategy - // const tx = amoStrategy - // .connect(vaultSigner) - // .withdraw(oSonicVault.address, assetToken.address, parseUnits("10")); - - // await expect(tx).to.be.revertedWith("Protocol insolvent"); - // }); - // it("Should withdraw all", async () => { - // const { vaultSigner, amoStrategy } = fixture; - - // // Vault withdraw alls from the strategy - // const tx = amoStrategy.connect(vaultSigner).withdrawAll(); - - // await expect(tx).to.not.revertedWith("Protocol insolvent"); - // }); - // it("Should fail to swap assets to the pool", async () => { - // const { amoStrategy, strategist } = fixture; - - // const tx = amoStrategy - // .connect(strategist) - // .swapAssetsToPool(parseUnits("10")); - - // await expect(tx).to.be.revertedWith("Protocol insolvent"); - // }); - // it("Should fail to swap OS to the pool", async () => { - // const { amoStrategy, strategist } = fixture; - - // const tx = amoStrategy - // .connect(strategist) - // .swapOTokensToPool(parseUnits("10")); - - // await expect(tx).to.be.revertedWith("Protocol insolvent"); - // }); - // }); + describe("with a little more OToken in the pool", () => { + beforeEach(async () => { + context = await contextFunction(); + fixture = await context.loadFixture({ + assetMintAmount: 20000, + depositToStrategy: true, + balancePool: true, + poolAddOTokenAmount: 5000, + }); + }); + it("Vault should deposit asset token to AMO strategy", async function () { + await assertDeposit(parseUnits("12000")); + }); + it("Vault should be able to withdraw all", async () => { + await assertWithdrawAll(); + }); + it("Vault should be able to partially withdraw", async () => { + await assertWithdrawPartial(parseUnits("1000")); + }); + it("Strategist should swap a little assets to the pool", async () => { + await assertSwapAssetsToPool(parseUnits("3")); + }); + it("Strategist should swap enough asset token to get the pool close to balanced", async () => { + const { assetReserves, oTokenReserves } = await getPoolReserves(); + // 50% of the extra OToken in the pool gets close to balanced + const oTokenAmount = oTokenReserves.sub(assetReserves).mul(50).div(100); + const assetAmount = oTokenAmount.mul(assetReserves).div(oTokenReserves); + + await assertSwapAssetsToPool(assetAmount); + }); + it("Strategist should fail to add too much asset token to the pool", async () => { + const { amoStrategy, strategist } = fixture; + + const dataBefore = await snapData(); + await logSnapData(dataBefore, "Before swapping assets to the pool"); + + // Try swapping too much asset token in. + const tx = amoStrategy + .connect(strategist) + .swapAssetsToPool(parseUnits("5000")); + + await expect(tx).to.be.revertedWith("Assets overshot peg"); + }); + it("Strategist should fail to add zero asset token to the pool", async () => { + const { amoStrategy, strategist } = fixture; + + const tx = amoStrategy.connect(strategist).swapAssetsToPool(0); + + await expect(tx).to.be.revertedWith("Must swap something"); + }); + it("Strategist should fail to add more OToken to the pool", async () => { + const { amoStrategy, strategist } = fixture; + + const tx = amoStrategy + .connect(strategist) + .swapOTokensToPool(parseUnits("0.001")); + + await expect(tx).to.be.revertedWith("OTokens balance worse"); + }); + }); + + describe("with a lot more asset token in the pool", () => { + beforeEach(async () => { + context = await contextFunction(); + fixture = await context.loadFixture({ + assetMintAmount: 5000, + depositToStrategy: true, + balancePool: true, + poolAddAssetAmount: 2000000, + }); + }); + it("Vault should fail to deposit asset token to strategy", async function () { + await assertFailedDeposit(parseUnits("6000"), "price out of range"); + }); + it("Vault should be able to withdraw all", async () => { + await assertWithdrawAll(); + }); + it("Vault should be able to partially withdraw", async () => { + await assertWithdrawPartial(parseUnits("1000")); + }); + it("Strategist should swap a little OToken to the pool", async () => { + await assertSwapOTokensToPool(parseUnits("0.3")); + }); + it("Strategist should swap a lot of OToken to the pool", async () => { + await assertSwapOTokensToPool(parseUnits("5000")); + }); + it("Strategist should get the pool close to balanced", async () => { + const { assetReserves, oTokenReserves } = await getPoolReserves(); + // 32% of the extra asset token in the pool gets pretty close to balanced + const oTokenAmount = assetReserves.sub(oTokenReserves).mul(32).div(100); + + await assertSwapOTokensToPool(oTokenAmount); + }); + it("Strategist should fail to add so much OToken that it overshoots", async () => { + const { amoStrategy, strategist } = fixture; + + const tx = amoStrategy + .connect(strategist) + .swapOTokensToPool(parseUnits("999990")); + + await expect(tx).to.be.revertedWith("OTokens overshot peg"); + }); + it("Strategist should fail to add more asset token to the pool", async () => { + const { amoStrategy, strategist } = fixture; + + const tx = amoStrategy + .connect(strategist) + .swapAssetsToPool(parseUnits("0.0001")); + + await expect(tx).to.be.revertedWith("Assets balance worse"); + }); + }); + + describe("with a little more asset token in the pool", () => { + beforeEach(async () => { + context = await contextFunction(); + fixture = await context.loadFixture({ + assetMintAmount: 20000, + depositToStrategy: true, + balancePool: true, + poolAddAssetAmount: 20000, + }); + }); + it("Vault should deposit asset token to AMO strategy", async function () { + await assertDeposit(parseUnits("18000")); + }); + it("Vault should be able to withdraw all", async () => { + await assertWithdrawAll(); + }); + it("Vault should be able to partially withdraw", async () => { + await assertWithdrawPartial(parseUnits("1000")); + }); + it("Strategist should swap a little OToken to the pool", async () => { + await assertSwapOTokensToPool(parseUnits("8")); + }); + it("Strategist should get the pool close to balanced", async () => { + const { assetReserves, oTokenReserves } = await getPoolReserves(); + // 50% of the extra asset token in the pool gets pretty close to balanced + const oTokenAmount = assetReserves.sub(oTokenReserves).mul(50).div(100); + + await assertSwapOTokensToPool(oTokenAmount); + }); + it("Strategist should fail to add zero OToken to the pool", async () => { + const { amoStrategy, strategist } = fixture; + + const tx = amoStrategy.connect(strategist).swapOTokensToPool(0); + + await expect(tx).to.be.revertedWith("Must swap something"); + }); + it("Strategist should fail to add too much OToken to the pool", async () => { + const { amoStrategy, strategist } = fixture; + + const tx = amoStrategy + .connect(strategist) + .swapOTokensToPool(parseUnits("11000")); + + await expect(tx).to.be.revertedWith("OTokens overshot peg"); + }); + it("Strategist should fail to add more asset token to the pool", async () => { + const { amoStrategy, strategist } = fixture; + + const tx = amoStrategy + .connect(strategist) + .swapAssetsToPool(parseUnits("0.0001")); + + await expect(tx).to.be.revertedWith("Assets balance worse"); + }); + }); + + describe("with the strategy owning a small percentage of the pool", () => { + let dataBefore; + + beforeEach(async () => { + context = await contextFunction(); + fixture = await context.loadFixture({ + assetMintAmount: 5000, + depositToStrategy: true, + balancePool: true, + }); + + const { nick, oToken, pool, assetToken } = fixture; + + // Other users add a lot more liquidity to the pool. + const bigAmount = parseUnits("1000000"); + // Acquire OToken by swapping asset token in, then add balanced-like liquidity + // while preserving enough OToken for the swap-heavy tests that follow. + await poolSwapTokensIn(assetToken, parseUnits("10000000")); + const oTokenBalance = await oToken.balanceOf(nick.address); + const oTokenBufferForTests = parseUnits("2000000"); + const oTokenToPool = oTokenBalance.sub(oTokenBufferForTests); + expect(oTokenToPool).to.gt(0); + + await assetToken.connect(nick).transfer(pool.address, bigAmount); + await oToken.connect(nick).transfer(pool.address, oTokenToPool); + await pool.connect(nick).mint(nick.address); + + dataBefore = await snapData(); + await logSnapData(dataBefore); + }); + + it("A lot of OToken is swapped into the pool", async () => { + const { oToken, amoStrategy, assetToken } = fixture; + + // Swap OToken into the pool and asset token out. + await poolSwapTokensIn(oToken, parseUnits("1005000")); + await logSnapData(await snapData(), "\nAfter swapping OToken into the pool"); + + expect( + await amoStrategy.checkBalance(assetToken.address), + "Strategy's check balance" + ).to.withinRange(dataBefore.stratBalance, dataBefore.stratBalance.add(1)); + + // Swap asset token into the pool and OToken out. + await poolSwapTokensIn(assetToken, parseUnits("2000000")); + await logSnapData( + await snapData(), + "\nAfter swapping asset token into the pool" + ); + + expect( + await amoStrategy.checkBalance(assetToken.address), + "Strategy's check balance" + ).to.withinRange(dataBefore.stratBalance, dataBefore.stratBalance.add(1)); + }); + + it("A lot of asset token is swapped into the pool", async () => { + const { amoStrategy, oToken, assetToken } = fixture; + + // Swap asset token into the pool and OToken out. + await poolSwapTokensIn(assetToken, parseUnits("1006000")); + await logSnapData( + await snapData(), + "\nAfter swapping asset token into the pool" + ); + + expect( + await amoStrategy.checkBalance(assetToken.address), + "Strategy's check balance" + ).to.withinRange(dataBefore.stratBalance, dataBefore.stratBalance.add(1)); + + // Swap OToken into the pool and asset token out. + await poolSwapTokensIn(oToken, parseUnits("1005000")); + await logSnapData(await snapData(), "\nAfter swapping OToken into the pool"); + + expect( + await amoStrategy.checkBalance(assetToken.address), + "Strategy's check balance" + ).to.withinRange(dataBefore.stratBalance, dataBefore.stratBalance.add(1)); + }); + }); + + describe("with an insolvent vault", () => { + beforeEach(async () => { + context = await contextFunction(); + fixture = await context.loadFixture({ + assetMintAmount: 5000000, + depositToStrategy: false, + }); + + const { vault, vaultSigner, amoStrategy, assetToken } = fixture; + + // Deposit a little to the strategy. + const littleAmount = parseUnits("100"); + await assetToken + .connect(vaultSigner) + .transfer(amoStrategy.address, littleAmount); + await amoStrategy + .connect(vaultSigner) + .deposit(assetToken.address, littleAmount); + + const totalAssets = await vault.totalValue(); + // Calculate a 0.21% (21 basis points) loss. + const lossAmount = totalAssets.mul(21).div(10000); + await assetToken.connect(vaultSigner).transfer(addresses.dead, lossAmount); + expect( + await assetToken.balanceOf(vault.address), + "Must have enough asset token in vault to make insolvent" + ).to.gte(lossAmount); + }); + + it("Should fail to deposit", async () => { + const { vaultSigner, amoStrategy, assetToken } = fixture; + + // Vault calls deposit on the strategy. + const depositAmount = parseUnits("10"); + await assetToken + .connect(vaultSigner) + .transfer(amoStrategy.address, depositAmount); + const tx = amoStrategy + .connect(vaultSigner) + .deposit(assetToken.address, depositAmount); + + await expect(tx).to.be.revertedWith("Protocol insolvent"); + }); + + it("Should fail to withdraw", async () => { + const { vault, vaultSigner, amoStrategy, assetToken } = fixture; + + // Vault withdraws from the strategy. + const tx = amoStrategy + .connect(vaultSigner) + .withdraw(vault.address, assetToken.address, parseUnits("10")); + + await expect(tx).to.be.revertedWith("Protocol insolvent"); + }); + + it("Should withdraw all", async () => { + const { vaultSigner, amoStrategy } = fixture; + + const tx = amoStrategy.connect(vaultSigner).withdrawAll(); + + await expect(tx).to.not.revertedWith("Protocol insolvent"); + }); + + it("Should fail to swap assets to the pool", async () => { + const { amoStrategy, strategist } = fixture; + + const tx = amoStrategy + .connect(strategist) + .swapAssetsToPool(parseUnits("10")); + + await expect(tx).to.be.revertedWith("Protocol insolvent"); + }); + + it("Should fail to swap OToken to the pool", async () => { + const { amoStrategy, strategist } = fixture; + + const tx = amoStrategy + .connect(strategist) + .swapOTokensToPool(parseUnits("10")); + + await expect(tx).to.be.revertedWith("Protocol insolvent"); + }); + }); const poolSwapTokensIn = async (tokenIn, amountIn) => { - const { nick, pool, assetToken, oTokenPoolIndex } = context; + const { nick, pool, assetToken, oTokenPoolIndex } = fixture; const amountOut = await pool.getAmountOut(amountIn, tokenIn.address); await tokenIn.connect(nick).transfer(pool.address, amountIn); @@ -1083,7 +1077,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { const snapData = async () => { const { vault, amoStrategy, oToken, pool, gauge, assetToken } = - context; + fixture; const stratBalance = await amoStrategy.checkBalance(assetToken.address); const oTokenSupply = await oToken.totalSupply(); @@ -1170,7 +1164,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { }; const logProfit = async (dataBefore) => { - const { oToken, vault, amoStrategy, assetToken } = context; + const { oToken, vault, amoStrategy, assetToken } = fixture; const stratBalanceAfter = await amoStrategy.checkBalance(assetToken.address); const oTokenSupplyAfter = await oToken.totalSupply(); @@ -1200,8 +1194,8 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { }; const assertChangedData = async (dataBefore, delta) => { - const { oToken, vault, amoStrategy, pool, gauge, assetToken } = - context; + const { oToken, vault, amoStrategy, gauge, assetToken } = + fixture; if (delta.stratBalance != undefined) { const expectedStratBalance = dataBefore.stratBalance.add( @@ -1274,8 +1268,9 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { pool, vaultSigner, assetToken, - } = context; - + } = fixture; + + await assetToken.connect(nick).approve(vault.address, assetDepositAmount); await vault.connect(nick).mint(assetToken.address, assetDepositAmount, 0); const dataBefore = await snapData(); @@ -1332,7 +1327,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { } async function assertFailedDeposit(assetDepositAmount, errorMessage) { - const { assetToken, vaultSigner, amoStrategy } = context; + const { assetToken, vaultSigner, amoStrategy } = fixture; const dataBefore = await snapData(); await logSnapData(dataBefore, "\nBefore depositing asset token to strategy"); @@ -1351,7 +1346,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { } async function assertFailedDepositAll(assetDepositAmount, errorMessage) { - const { assetToken, vaultSigner, amoStrategy } = context; + const { assetToken, vaultSigner, amoStrategy } = fixture; const dataBefore = await snapData(); await logSnapData(dataBefore, "\nBefore depositing all wS to strategy"); @@ -1369,7 +1364,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { async function assertWithdrawAll() { const { amoStrategy, pool, oToken, vaultSigner, assetToken } = - context; + fixture; const dataBefore = await snapData(); await logSnapData(dataBefore); @@ -1427,7 +1422,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { vault, vaultSigner, assetToken, - } = context; + } = fixture; const dataBefore = await snapData(); @@ -1492,7 +1487,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { } async function assertSwapAssetsToPool(assetAmount) { - const { oToken, amoStrategy, pool, strategist, assetToken } = context; + const { oToken, amoStrategy, pool, strategist, assetToken } = fixture; const dataBefore = await snapData(); await logSnapData( @@ -1554,21 +1549,21 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { ).to.equal(0); } - async function assertSwapOTokensToPool(osAmount) { - const { oSonic, pool, amoStrategy, strategist } = fixture; + async function assertSwapOTokensToPool(oTokenAmount) { + const { oToken, pool, amoStrategy, strategist } = fixture; const dataBefore = await snapData(); await logSnapData(dataBefore, "Before swapping OTokens to the pool"); - // Mint OS and swap into the pool, then mint more OS to add with the wS swapped out + // Mint OToken and swap into the pool, then mint more OToken to add with the swapped out asset token. const tx = await amoStrategy .connect(strategist) - .swapOTokensToPool(osAmount); + .swapOTokensToPool(oTokenAmount); // Check emitted event await expect(tx) .emit(amoStrategy, "SwapOTokensToPool") - .withNamedArgs({ oTokenMinted: osAmount }); + .withNamedArgs({ oTokenMinted: oTokenAmount }); await logSnapData(await snapData(), "\nAfter swapping OTokens to the pool"); await logProfit(dataBefore); @@ -1577,7 +1572,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { dataBefore, { // stratBalance: osBurnAmount.mul(-1), - vaultWSBalance: 0, + vaultAssetBalance: 0, stratGaugeBalance: 0, }, fixture @@ -1588,14 +1583,14 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { "Strategy's pool LP balance" ).to.equal(0); expect( - await oSonic.balanceOf(amoStrategy.address), - "Strategy's OS balance" + await oToken.balanceOf(amoStrategy.address), + "Strategy's OToken balance" ).to.equal(0); } // Calculate the minted OS amount for a deposit async function calcOTokenMintAmount(assetDepositAmount) { - const { pool } = context; + const { pool } = fixture; const { assetReserves, oTokenReserves } = await getPoolReserves(); @@ -1609,7 +1604,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { } async function getPoolReserves() { - const { pool, oTokenPoolIndex } = context; + const { pool, oTokenPoolIndex } = fixture; let assetReserves, oTokenReserves; // Get the reserves of the pool @@ -1622,7 +1617,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { } // Calculate the amount of oToken burnt from a withdraw async function calcOTokenWithdrawAmount(assetTokenWithdrawAmount) { - const { pool } = context; + const { pool } = fixture; const { assetReserves, oTokenReserves } = await getPoolReserves(); @@ -1642,7 +1637,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { // Calculate the OToken and asset token amounts from a withdrawAll async function calcWithdrawAllAmounts() { - const { amoStrategy, gauge, pool } = context; + const { amoStrategy, gauge, pool } = fixture; // Get the reserves of the pool const { assetReserves, oTokenReserves } = await getPoolReserves(); diff --git a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js index d85224bf13..07b579aa82 100644 --- a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js +++ b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js @@ -3,41 +3,46 @@ const addresses = require("../../../utils/addresses"); const { shouldBehaveLikeAlgebraAmoStrategy } = require("../../behaviour/algebraAmoStrategy"); const { createFixtureLoader } = require("../../_fixture"); -describe.only("Sonic Fork Test: Sonic Staking Strategy", function () { - shouldBehaveLikeAlgebraAmoStrategy(async() => { - const fixture = await swapXAMOFixture(); +describe("Sonic Fork Test: SwapX AMO Strategy", function () { + shouldBehaveLikeAlgebraAmoStrategy(async () => { return { - fixture: swapXAMOFixture, - loadFixture: ({ + loadFixture: async ({ assetMintAmount = 0, depositToStrategy = false, balancePool = false, poolAddAssetAmount = 0, poolAddOTokenAmount = 0 } = {}) => { - return createFixtureLoader(swapXAMOFixture, { + const fixtureLoader = await createFixtureLoader(swapXAMOFixture, { wsMintAmount: assetMintAmount, depositToStrategy, balancePool, poolAddwSAmount: poolAddAssetAmount, poolAddOSAmount: poolAddOTokenAmount }); + + const fixture = await fixtureLoader(); + const oTokenPoolIndex = + (await fixture.swapXPool.token0()) === fixture.oSonic.address ? 0 : 1; + + return { + addresses: addresses.sonic, + assetToken: fixture.wS, // address of the asset token in the pool + oToken: fixture.oSonic, // address of the oToken in the pool + rewardToken: fixture.swpx, // address of the reward token + amoStrategy: fixture.swapXAMOStrategy, // address of the strategy + pool: fixture.swapXPool, + gauge: fixture.swapXGauge, // address of the gauge + governor: fixture.governor, // address of the governor + timelock: fixture.governor, // address of the timelock + strategist: fixture.strategist, // address of the strategist + nick: fixture.nick, // nick's address + oTokenPoolIndex, // index of the oToken in the pool + vaultSigner: fixture.oSonicVaultSigner, // address of the vault signer + vault: fixture.oSonicVault, // address of the vault + harvester: fixture.harvester, // address of the harvester + }; }, - addresses: addresses.sonic, - assetToken: fixture.wS, // address of the asset token in the pool - oToken: fixture.oSonic, // address of the oToken in the pool - rewardToken: fixture.swpx, // address of the reward token - amoStrategy: fixture.swapXAMOStrategy, // address of the strategy - pool: fixture.swapXPool, - gauge: fixture.swapXGauge, // address of the gauge - governor: fixture.governor, // address of the governor - timelock: fixture.governor, // address of the timelock - strategist: fixture.strategist, // address of the strategist - nick: fixture.nick, // nick's address - oTokenPoolIndex: 1, // index of the oToken in the pool - vaultSigner: fixture.oSonicVaultSigner, // address of the vault signer - vault: fixture.oSonicVault, // address of the vault - harvester: fixture.harvester, // address of the harvester }; }); }); \ No newline at end of file From 52069262983016cac41ff77953405455036d2eaa Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 17 Feb 2026 00:04:39 +0100 Subject: [PATCH 12/44] prepare supernova fixture --- contracts/test/_fixture.js | 154 ++++++++++++++++++++++++++++++++++++- 1 file changed, 151 insertions(+), 3 deletions(-) diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 505900443e..75261399d8 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -2567,11 +2567,159 @@ async function instantRebaseVaultFixture(tokenName) { return fixture; } -async function supernovaOETHAMOFixure() { +async function supernovaOETHAMOFixure( + config = { + assetMintAmount: 0, + depositToStrategy: false, + balancePool: false, + poolAddAssetAmount: 0, + poolAddOTokenAmount: 0, + } +) { const fixture = await defaultFixture(); - //const { oeth, weth } = fixture; + const { oeth, oethVault, weth, josh, strategist } = fixture; - return fixture; + if (!isFork) { + throw new Error("supernovaOETHAMOFixure is only supported on fork tests"); + } + + const cfg = { + assetMintAmount: config?.assetMintAmount || 0, + depositToStrategy: config?.depositToStrategy || false, + balancePool: config?.balancePool || false, + poolAddAssetAmount: config?.poolAddAssetAmount || 0, + poolAddOTokenAmount: config?.poolAddOTokenAmount || 0, + }; + + const cOETHSupernovaAMOProxy = await ethers.getContract("OETHSupernovaAMOProxy"); + const cOETHSupernovaAMOStrategy = await ethers.getContractAt( + "OETHSupernovaAMOStrategy", + cOETHSupernovaAMOProxy.address + ); + + const supernovaPool = await ethers.getContractAt( + "IPair", + await cOETHSupernovaAMOStrategy.pool() + ); + const supernovaGauge = await ethers.getContractAt( + "IGauge", + await cOETHSupernovaAMOStrategy.gauge() + ); + const supernovaRewardToken = await ethers.getContractAt( + erc20Abi, + addresses.mainnet.supernovaToken + ); + + // Impersonate the OETH Vault to call strategy deposit/withdraw methods directly in tests. + const oethVaultSigner = await impersonateAndFund(oethVault.address); + const oethVaultGovernor = await impersonateAndFund(await oethVault.governor()); + + // Ensure the test actor has enough WETH to mint OETH and manipulate pool balances. + await setERC20TokenBalance(josh.address, weth, oethUnits("1000000000"), hre); + await resetAllowance(weth, josh, oethVault.address); + + // Supernova deployment creates a fresh empty pool, seed it once for AMO tests. + if ((await supernovaPool.totalSupply()).eq(0)) { + const seedAmount = parseUnits("2000"); + await oethVault.connect(josh).mint(weth.address, seedAmount.mul(2), 0); + await weth.connect(josh).transfer(supernovaPool.address, seedAmount); + await oeth.connect(josh).transfer(supernovaPool.address, seedAmount); + await supernovaPool.connect(josh).mint(josh.address); + } + + // Mint some OETH using WETH if configured. + if (cfg.assetMintAmount > 0) { + const wethAmount = parseUnits(cfg.assetMintAmount.toString()); + await oethVault.connect(josh).rebase(); + await oethVault.connect(josh).allocate(); + + let wethBalance = await weth.balanceOf(oethVault.address); + const autoAllocateThreshold = await oethVault.autoAllocateThreshold(); + const queue = await oethVault.withdrawalQueueMetadata(); + const available = wethBalance.add(queue.claimed).sub(queue.queued); + const mintAmount = wethAmount.sub(available); + + if (mintAmount.gt(0)) { + await weth.connect(josh).approve(oethVault.address, mintAmount); + + const disableAutoAllocate = autoAllocateThreshold.lt(mintAmount); + if (disableAutoAllocate) { + await oethVault + .connect(oethVaultGovernor) + .setAutoAllocateThreshold(mintAmount.add(1)); + } + + // This mints OETH and keeps backing WETH in the vault. + await oethVault.connect(josh).mint(weth.address, mintAmount, 0); + + if (disableAutoAllocate) { + await oethVault + .connect(oethVaultGovernor) + .setAutoAllocateThreshold(autoAllocateThreshold); + } + } + + if (cfg.depositToStrategy) { + wethBalance = await weth.balanceOf(oethVault.address); + log( + `Depositing ${formatUnits( + wethAmount + )} WETH to Supernova AMO strategy. Vault has ${formatUnits( + wethBalance + )} WETH` + ); + await oethVault + .connect(strategist) + .depositToStrategy( + cOETHSupernovaAMOStrategy.address, + [weth.address], + [wethAmount] + ); + } + } + + if (cfg.balancePool) { + const { _reserve0, _reserve1 } = await supernovaPool.getReserves(); + const oTokenPoolIndex = + (await supernovaPool.token0()) === oeth.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.poolAddOTokenAmount += diff; + } else if (diff < 0) { + cfg.poolAddAssetAmount += -diff; + } + } + + // Add WETH to the pool directly. + if (cfg.poolAddAssetAmount > 0) { + const wethAmount = parseUnits(cfg.poolAddAssetAmount.toString(), 18); + await weth.connect(josh).transfer(supernovaPool.address, wethAmount); + } + + // Add OETH to the pool directly. + if (cfg.poolAddOTokenAmount > 0) { + const oethAmount = parseUnits(cfg.poolAddOTokenAmount.toString(), 18); + await oethVault.connect(josh).mint(weth.address, oethAmount, 0); + await oeth.connect(josh).transfer(supernovaPool.address, oethAmount); + } + + // Force reserves to match balances. + await supernovaPool.sync(); + + return { + ...fixture, + oethVaultSigner, + supernovaRewardToken, + supernovaPool, + supernovaGauge, + supernovaAMOStrategy: cOETHSupernovaAMOStrategy, + }; } // Unit test cross chain fixture where both contracts are deployed on the same chain for the From 6cda7e36d477c36ea76f9191e6c9bbdb2ace3827 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Tue, 17 Feb 2026 00:05:30 +0100 Subject: [PATCH 13/44] separate pool tilt config into a separate file --- .../test/behaviour/algebraAmoStrategy.js | 458 +++++++++++++++--- .../oeth-supernova-amo.mainnet.fork-test.js | 119 +++++ .../sonic/swapx-amo.sonic.fork-test.js | 69 ++- 3 files changed, 587 insertions(+), 59 deletions(-) create mode 100644 contracts/test/strategies/oeth-supernova-amo.mainnet.fork-test.js diff --git a/contracts/test/behaviour/algebraAmoStrategy.js b/contracts/test/behaviour/algebraAmoStrategy.js index 0fba8ba5bb..61f08c6a38 100644 --- a/contracts/test/behaviour/algebraAmoStrategy.js +++ b/contracts/test/behaviour/algebraAmoStrategy.js @@ -6,6 +6,149 @@ const { setERC20TokenBalance } = require("../_fund"); const { isCI } = require("../helpers"); const log = require("../../utils/logger")("test:fork:algebra:amo"); + +const defaultScenarioConfig = { + attackerFrontRun: { + moderateAssetIn: "20000", + largeAssetIn: "10000000", + largeOTokenIn: "10000000", + }, + poolImbalance: { + lotMoreOToken: { addOToken: 1000000 }, + littleMoreOToken: { addOToken: 5000 }, + lotMoreAsset: { addAsset: 2000000 }, + littleMoreAsset: { addAsset: 20000 }, + }, + smallPoolShare: { + bootstrapAssetSwapIn: "10000000", + bigLiquidityAsset: "1000000", + oTokenBuffer: "2000000", + stressSwapOToken: "1005000", + stressSwapAsset: "2000000", + stressSwapAssetAlt: "1006000", + }, + rebalanceProbe: { + frontRun: { + depositAmount: "200000", + failedDepositAmount: "5000", + failedDepositAllAmount: "5000", + assetTiltWithdrawAmount: "4000", + oTokenTiltWithdrawAmount: "200", + }, + lotMoreOToken: { + failedDepositAmount: "5000", + partialWithdrawAmount: "4000", + smallSwapAssetsToPool: "3", + largeSwapAssetsToPool: "3000", + nearMaxSwapAssetsToPool: "4400", + excessiveSwapAssetsToPool: "2000000", + disallowedSwapOTokensToPool: "0.001", + }, + littleMoreOToken: { + depositAmount: "12000", + partialWithdrawAmount: "1000", + smallSwapAssetsToPool: "3", + excessiveSwapAssetsToPool: "5000", + disallowedSwapOTokensToPool: "0.001", + }, + lotMoreAsset: { + failedDepositAmount: "6000", + partialWithdrawAmount: "1000", + smallSwapOTokensToPool: "0.3", + largeSwapOTokensToPool: "5000", + overshootSwapOTokensToPool: "999990", + disallowedSwapAssetsToPool: "0.0001", + }, + littleMoreAsset: { + depositAmount: "18000", + partialWithdrawAmount: "1000", + smallSwapOTokensToPool: "8", + overshootSwapOTokensToPool: "11000", + disallowedSwapAssetsToPool: "0.0001", + }, + }, + insolvent: { + swapOTokensToPool: "10", + }, + fixtureSetup: { + imbalanceBalancePool: true, + }, +}; + +const mergeScenarioConfig = (contextConfig = {}, fixtureConfig = {}) => ({ + attackerFrontRun: { + ...defaultScenarioConfig.attackerFrontRun, + ...(contextConfig.attackerFrontRun || {}), + ...(fixtureConfig.attackerFrontRun || {}), + }, + poolImbalance: { + lotMoreOToken: { + ...defaultScenarioConfig.poolImbalance.lotMoreOToken, + ...(contextConfig.poolImbalance?.lotMoreOToken || {}), + ...(fixtureConfig.poolImbalance?.lotMoreOToken || {}), + }, + littleMoreOToken: { + ...defaultScenarioConfig.poolImbalance.littleMoreOToken, + ...(contextConfig.poolImbalance?.littleMoreOToken || {}), + ...(fixtureConfig.poolImbalance?.littleMoreOToken || {}), + }, + lotMoreAsset: { + ...defaultScenarioConfig.poolImbalance.lotMoreAsset, + ...(contextConfig.poolImbalance?.lotMoreAsset || {}), + ...(fixtureConfig.poolImbalance?.lotMoreAsset || {}), + }, + littleMoreAsset: { + ...defaultScenarioConfig.poolImbalance.littleMoreAsset, + ...(contextConfig.poolImbalance?.littleMoreAsset || {}), + ...(fixtureConfig.poolImbalance?.littleMoreAsset || {}), + }, + }, + smallPoolShare: { + ...defaultScenarioConfig.smallPoolShare, + ...(contextConfig.smallPoolShare || {}), + ...(fixtureConfig.smallPoolShare || {}), + }, + rebalanceProbe: { + frontRun: { + ...defaultScenarioConfig.rebalanceProbe.frontRun, + ...(contextConfig.rebalanceProbe?.frontRun || {}), + ...(fixtureConfig.rebalanceProbe?.frontRun || {}), + }, + lotMoreOToken: { + ...defaultScenarioConfig.rebalanceProbe.lotMoreOToken, + ...(contextConfig.rebalanceProbe?.lotMoreOToken || {}), + ...(fixtureConfig.rebalanceProbe?.lotMoreOToken || {}), + }, + littleMoreOToken: { + ...defaultScenarioConfig.rebalanceProbe.littleMoreOToken, + ...(contextConfig.rebalanceProbe?.littleMoreOToken || {}), + ...(fixtureConfig.rebalanceProbe?.littleMoreOToken || {}), + }, + lotMoreAsset: { + ...defaultScenarioConfig.rebalanceProbe.lotMoreAsset, + ...(contextConfig.rebalanceProbe?.lotMoreAsset || {}), + ...(fixtureConfig.rebalanceProbe?.lotMoreAsset || {}), + }, + littleMoreAsset: { + ...defaultScenarioConfig.rebalanceProbe.littleMoreAsset, + ...(contextConfig.rebalanceProbe?.littleMoreAsset || {}), + ...(fixtureConfig.rebalanceProbe?.littleMoreAsset || {}), + }, + }, + insolvent: { + ...defaultScenarioConfig.insolvent, + ...(contextConfig.insolvent || {}), + ...(fixtureConfig.insolvent || {}), + }, + fixtureSetup: { + ...defaultScenarioConfig.fixtureSetup, + ...(contextConfig.fixtureSetup || {}), + ...(fixtureConfig.fixtureSetup || {}), + }, +}); + +const toUnitAmount = (value) => + value && value._isBigNumber ? value : parseUnits(value.toString()); /** * * @param {*} context a function that returns a fixture with the additional properties: @@ -29,6 +172,8 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { // Retry up to 3 times on CI this.retries(isCI ? 3 : 0); let fixture, context; + const getScenarioConfig = () => + mergeScenarioConfig(context?.scenarioConfig, fixture?.scenarioConfig); describe("post deployment", () => { beforeEach(async () => { context = await contextFunction(); @@ -301,7 +446,11 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { const tx = amoStrategy.connect(timelock).withdrawAll(); await expect(tx).to.emit(amoStrategy, "Withdrawal"); }); - it("Harvester can collect rewards", async () => { + it("Harvester can collect rewards", async function () { + if (fixture.skipHarvesterTest) { + this.skip(); + } + const { harvester, nick, @@ -337,12 +486,18 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { ); expect(rewardTokenBalanceAfter).to.gt(rewardTokenBalanceBefore); }); - it("Attacker front-run deposit within range by adding asset token to the pool", async () => { + it("Attacker front-run deposit within range by adding asset token to the pool", async function () { + if (fixture.skipAdvancedRebalanceTests) { + this.skip(); + } + const { nick, oToken, vaultSigner, amoStrategy, assetToken } = fixture; const attackerAssetTokenBalanceBefore = await assetToken.balanceOf(nick.address); - const assetTokenAmountIn = parseUnits("20000"); + const assetTokenAmountIn = toUnitAmount( + getScenarioConfig().attackerFrontRun.moderateAssetIn + ); const dataBeforeSwap = await snapData(); logSnapData( @@ -356,7 +511,9 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { // This drops the pool's asset token/OToken price and increases the OToken/asset token price const oTokenAmountOut = await poolSwapTokensIn(assetToken, assetTokenAmountIn); - const depositAmount = parseUnits("200000"); + const depositAmount = toUnitAmount( + getScenarioConfig().rebalanceProbe.frontRun.depositAmount + ); const dataBeforeDeposit = await snapData(); logSnapData( @@ -367,6 +524,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { ); // Vault deposits wS to the strategy + await ensureVaultHasAssets(depositAmount); await assetToken .connect(vaultSigner) .transfer(amoStrategy.address, depositAmount); @@ -409,13 +567,18 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { let attackerAssetBalanceBefore; let dataBeforeSwap; let oTokenAmountOut; - beforeEach(async () => { + beforeEach(async function () { context = await contextFunction(); + if (context.skipAdvancedRebalanceTests) { + this.skip(); + } fixture = await context.loadFixture(); const { nick, assetToken } = fixture; attackerAssetBalanceBefore = await assetToken.balanceOf(nick.address); - const assetTokenAmountIn = parseUnits("10000000"); + const assetTokenAmountIn = toUnitAmount( + getScenarioConfig().attackerFrontRun.largeAssetIn + ); dataBeforeSwap = await snapData(); logSnapData( @@ -430,10 +593,20 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { oTokenAmountOut = await poolSwapTokensIn(assetToken, assetTokenAmountIn); }); it("Strategist fails to deposit to strategy", async () => { - await assertFailedDeposit(parseUnits("5000"), "price out of range"); + await assertFailedDeposit( + toUnitAmount( + getScenarioConfig().rebalanceProbe.frontRun.failedDepositAmount + ), + "price out of range" + ); }); it("Strategist fails to deposit all to strategy", async () => { - await assertFailedDepositAll(parseUnits("5000"), "price out of range"); + await assertFailedDepositAll( + toUnitAmount( + getScenarioConfig().rebalanceProbe.frontRun.failedDepositAllAmount + ), + "price out of range" + ); }); it("Strategist should withdraw from strategy with a profit", async () => { const { @@ -444,7 +617,9 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { amoStrategy, assetToken, } = fixture; - const withdrawAmount = parseUnits("4000"); + const withdrawAmount = toUnitAmount( + getScenarioConfig().rebalanceProbe.frontRun.assetTiltWithdrawAmount + ); const dataBeforeWithdraw = await snapData(); logSnapData( @@ -495,12 +670,17 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { const attackerBalanceBefore = {}; let dataBeforeSwap; let assetAmountOut; - beforeEach(async () => { + beforeEach(async function () { context = await contextFunction(); + if (context.skipAdvancedRebalanceTests) { + this.skip(); + } fixture = await context.loadFixture(); const { nick, oToken, vault, assetToken } = fixture; - const oTokenAmountIn = parseUnits("10000000"); + const oTokenAmountIn = toUnitAmount( + getScenarioConfig().attackerFrontRun.largeOTokenIn + ); // Mint OToken using asset token await vault.connect(nick).mint(assetToken.address, oTokenAmountIn, 0); @@ -521,10 +701,20 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { assetAmountOut = await poolSwapTokensIn(oToken, oTokenAmountIn); }); it("Strategist fails to deposit to strategy", async () => { - await assertFailedDeposit(parseUnits("5000"), "price out of range"); + await assertFailedDeposit( + toUnitAmount( + getScenarioConfig().rebalanceProbe.frontRun.failedDepositAmount + ), + "price out of range" + ); }); it("Strategist fails to deposit all to strategy", async () => { - await assertFailedDepositAll(parseUnits("5000"), "price out of range"); + await assertFailedDepositAll( + toUnitAmount( + getScenarioConfig().rebalanceProbe.frontRun.failedDepositAllAmount + ), + "price out of range" + ); }); it("Strategist should withdraw from strategy with a profit", async () => { const { @@ -535,7 +725,9 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { amoStrategy, assetToken, } = fixture; - const withdrawAmount = parseUnits("200"); + const withdrawAmount = toUnitAmount( + getScenarioConfig().rebalanceProbe.frontRun.oTokenTiltWithdrawAmount + ); const dataBeforeWithdraw = await snapData(); logSnapData( @@ -589,26 +781,43 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { }); describe("with a lot more OToken in the pool", () => { - beforeEach(async () => { + beforeEach(async function () { context = await contextFunction(); + if (context.skipAdvancedRebalanceTests) { + this.skip(); + } fixture = await context.loadFixture({ assetMintAmount: 5000, depositToStrategy: true, - balancePool: true, - poolAddOTokenAmount: 1000000, + balancePool: getScenarioConfig().fixtureSetup.imbalanceBalancePool, + poolAddOTokenAmount: + getScenarioConfig().poolImbalance.lotMoreOToken.addOToken, }); }); it("Vault should fail to deposit asset token to AMO strategy", async function () { - await assertFailedDeposit(parseUnits("5000"), "price out of range"); + await assertFailedDeposit( + toUnitAmount( + getScenarioConfig().rebalanceProbe.lotMoreOToken.failedDepositAmount + ), + "price out of range" + ); }); it("Vault should be able to withdraw all", async () => { await assertWithdrawAll(); }); it("Vault should be able to partially withdraw", async () => { - await assertWithdrawPartial(parseUnits("4000")); + await assertWithdrawPartial( + toUnitAmount( + getScenarioConfig().rebalanceProbe.lotMoreOToken.partialWithdrawAmount + ) + ); }); it("Strategist should swap a little assets to the pool", async () => { - await assertSwapAssetsToPool(parseUnits("3")); + await assertSwapAssetsToPool( + toUnitAmount( + getScenarioConfig().rebalanceProbe.lotMoreOToken.smallSwapAssetsToPool + ) + ); }); it("Strategist should swap enough asset token to get the pool close to balanced", async () => { const { assetReserves, oTokenReserves } = await getPoolReserves(); @@ -621,18 +830,30 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { await assertSwapAssetsToPool(assetAmount); }); it("Strategist should swap a lot of assets to the pool", async () => { - await assertSwapAssetsToPool(parseUnits("3000")); + await assertSwapAssetsToPool( + toUnitAmount( + getScenarioConfig().rebalanceProbe.lotMoreOToken.largeSwapAssetsToPool + ) + ); }); it("Strategist should swap most of the asset token owned by the strategy", async () => { // TODO calculate how much asset token should be swapped to get the pool balanced - await assertSwapAssetsToPool(parseUnits("4400")); + await assertSwapAssetsToPool( + toUnitAmount( + getScenarioConfig().rebalanceProbe.lotMoreOToken.nearMaxSwapAssetsToPool + ) + ); }); it("Strategist should fail to add more asset token than owned by the strategy", async () => { const { amoStrategy, strategist } = fixture; const tx = amoStrategy .connect(strategist) - .swapAssetsToPool(parseUnits("2000000")); + .swapAssetsToPool( + toUnitAmount( + getScenarioConfig().rebalanceProbe.lotMoreOToken.excessiveSwapAssetsToPool + ) + ); await expect(tx).to.be.revertedWith("Not enough LP tokens in gauge"); }); @@ -642,33 +863,53 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { // Try swapping OToken into the pool. const tx = amoStrategy .connect(strategist) - .swapOTokensToPool(parseUnits("0.001")); + .swapOTokensToPool( + toUnitAmount( + getScenarioConfig().rebalanceProbe.lotMoreOToken.disallowedSwapOTokensToPool + ) + ); await expect(tx).to.be.revertedWith("OTokens balance worse"); }); }); describe("with a little more OToken in the pool", () => { - beforeEach(async () => { + beforeEach(async function () { context = await contextFunction(); + if (context.skipAdvancedRebalanceTests) { + this.skip(); + } fixture = await context.loadFixture({ assetMintAmount: 20000, depositToStrategy: true, - balancePool: true, - poolAddOTokenAmount: 5000, + balancePool: getScenarioConfig().fixtureSetup.imbalanceBalancePool, + poolAddOTokenAmount: + getScenarioConfig().poolImbalance.littleMoreOToken.addOToken, }); }); it("Vault should deposit asset token to AMO strategy", async function () { - await assertDeposit(parseUnits("12000")); + await assertDeposit( + toUnitAmount( + getScenarioConfig().rebalanceProbe.littleMoreOToken.depositAmount + ) + ); }); it("Vault should be able to withdraw all", async () => { await assertWithdrawAll(); }); it("Vault should be able to partially withdraw", async () => { - await assertWithdrawPartial(parseUnits("1000")); + await assertWithdrawPartial( + toUnitAmount( + getScenarioConfig().rebalanceProbe.littleMoreOToken.partialWithdrawAmount + ) + ); }); it("Strategist should swap a little assets to the pool", async () => { - await assertSwapAssetsToPool(parseUnits("3")); + await assertSwapAssetsToPool( + toUnitAmount( + getScenarioConfig().rebalanceProbe.littleMoreOToken.smallSwapAssetsToPool + ) + ); }); it("Strategist should swap enough asset token to get the pool close to balanced", async () => { const { assetReserves, oTokenReserves } = await getPoolReserves(); @@ -687,7 +928,11 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { // Try swapping too much asset token in. const tx = amoStrategy .connect(strategist) - .swapAssetsToPool(parseUnits("5000")); + .swapAssetsToPool( + toUnitAmount( + getScenarioConfig().rebalanceProbe.littleMoreOToken.excessiveSwapAssetsToPool + ) + ); await expect(tx).to.be.revertedWith("Assets overshot peg"); }); @@ -703,36 +948,61 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { const tx = amoStrategy .connect(strategist) - .swapOTokensToPool(parseUnits("0.001")); + .swapOTokensToPool( + toUnitAmount( + getScenarioConfig().rebalanceProbe.littleMoreOToken.disallowedSwapOTokensToPool + ) + ); await expect(tx).to.be.revertedWith("OTokens balance worse"); }); }); describe("with a lot more asset token in the pool", () => { - beforeEach(async () => { + beforeEach(async function () { context = await contextFunction(); + if (context.skipAdvancedRebalanceTests) { + this.skip(); + } fixture = await context.loadFixture({ assetMintAmount: 5000, depositToStrategy: true, - balancePool: true, - poolAddAssetAmount: 2000000, + balancePool: getScenarioConfig().fixtureSetup.imbalanceBalancePool, + poolAddAssetAmount: + getScenarioConfig().poolImbalance.lotMoreAsset.addAsset, }); }); it("Vault should fail to deposit asset token to strategy", async function () { - await assertFailedDeposit(parseUnits("6000"), "price out of range"); + await assertFailedDeposit( + toUnitAmount( + getScenarioConfig().rebalanceProbe.lotMoreAsset.failedDepositAmount + ), + "price out of range" + ); }); it("Vault should be able to withdraw all", async () => { await assertWithdrawAll(); }); it("Vault should be able to partially withdraw", async () => { - await assertWithdrawPartial(parseUnits("1000")); + await assertWithdrawPartial( + toUnitAmount( + getScenarioConfig().rebalanceProbe.lotMoreAsset.partialWithdrawAmount + ) + ); }); it("Strategist should swap a little OToken to the pool", async () => { - await assertSwapOTokensToPool(parseUnits("0.3")); + await assertSwapOTokensToPool( + toUnitAmount( + getScenarioConfig().rebalanceProbe.lotMoreAsset.smallSwapOTokensToPool + ) + ); }); it("Strategist should swap a lot of OToken to the pool", async () => { - await assertSwapOTokensToPool(parseUnits("5000")); + await assertSwapOTokensToPool( + toUnitAmount( + getScenarioConfig().rebalanceProbe.lotMoreAsset.largeSwapOTokensToPool + ) + ); }); it("Strategist should get the pool close to balanced", async () => { const { assetReserves, oTokenReserves } = await getPoolReserves(); @@ -746,7 +1016,11 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { const tx = amoStrategy .connect(strategist) - .swapOTokensToPool(parseUnits("999990")); + .swapOTokensToPool( + toUnitAmount( + getScenarioConfig().rebalanceProbe.lotMoreAsset.overshootSwapOTokensToPool + ) + ); await expect(tx).to.be.revertedWith("OTokens overshot peg"); }); @@ -755,33 +1029,53 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { const tx = amoStrategy .connect(strategist) - .swapAssetsToPool(parseUnits("0.0001")); + .swapAssetsToPool( + toUnitAmount( + getScenarioConfig().rebalanceProbe.lotMoreAsset.disallowedSwapAssetsToPool + ) + ); await expect(tx).to.be.revertedWith("Assets balance worse"); }); }); describe("with a little more asset token in the pool", () => { - beforeEach(async () => { + beforeEach(async function () { context = await contextFunction(); + if (context.skipAdvancedRebalanceTests) { + this.skip(); + } fixture = await context.loadFixture({ assetMintAmount: 20000, depositToStrategy: true, - balancePool: true, - poolAddAssetAmount: 20000, + balancePool: getScenarioConfig().fixtureSetup.imbalanceBalancePool, + poolAddAssetAmount: + getScenarioConfig().poolImbalance.littleMoreAsset.addAsset, }); }); it("Vault should deposit asset token to AMO strategy", async function () { - await assertDeposit(parseUnits("18000")); + await assertDeposit( + toUnitAmount( + getScenarioConfig().rebalanceProbe.littleMoreAsset.depositAmount + ) + ); }); it("Vault should be able to withdraw all", async () => { await assertWithdrawAll(); }); it("Vault should be able to partially withdraw", async () => { - await assertWithdrawPartial(parseUnits("1000")); + await assertWithdrawPartial( + toUnitAmount( + getScenarioConfig().rebalanceProbe.littleMoreAsset.partialWithdrawAmount + ) + ); }); it("Strategist should swap a little OToken to the pool", async () => { - await assertSwapOTokensToPool(parseUnits("8")); + await assertSwapOTokensToPool( + toUnitAmount( + getScenarioConfig().rebalanceProbe.littleMoreAsset.smallSwapOTokensToPool + ) + ); }); it("Strategist should get the pool close to balanced", async () => { const { assetReserves, oTokenReserves } = await getPoolReserves(); @@ -802,7 +1096,11 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { const tx = amoStrategy .connect(strategist) - .swapOTokensToPool(parseUnits("11000")); + .swapOTokensToPool( + toUnitAmount( + getScenarioConfig().rebalanceProbe.littleMoreAsset.overshootSwapOTokensToPool + ) + ); await expect(tx).to.be.revertedWith("OTokens overshot peg"); }); @@ -811,7 +1109,11 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { const tx = amoStrategy .connect(strategist) - .swapAssetsToPool(parseUnits("0.0001")); + .swapAssetsToPool( + toUnitAmount( + getScenarioConfig().rebalanceProbe.littleMoreAsset.disallowedSwapAssetsToPool + ) + ); await expect(tx).to.be.revertedWith("Assets balance worse"); }); @@ -820,8 +1122,11 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { describe("with the strategy owning a small percentage of the pool", () => { let dataBefore; - beforeEach(async () => { + beforeEach(async function () { context = await contextFunction(); + if (context.skipAdvancedRebalanceTests) { + this.skip(); + } fixture = await context.loadFixture({ assetMintAmount: 5000, depositToStrategy: true, @@ -831,12 +1136,19 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { const { nick, oToken, pool, assetToken } = fixture; // Other users add a lot more liquidity to the pool. - const bigAmount = parseUnits("1000000"); + const bigAmount = toUnitAmount( + getScenarioConfig().smallPoolShare.bigLiquidityAsset + ); // Acquire OToken by swapping asset token in, then add balanced-like liquidity // while preserving enough OToken for the swap-heavy tests that follow. - await poolSwapTokensIn(assetToken, parseUnits("10000000")); + await poolSwapTokensIn( + assetToken, + toUnitAmount(getScenarioConfig().smallPoolShare.bootstrapAssetSwapIn) + ); const oTokenBalance = await oToken.balanceOf(nick.address); - const oTokenBufferForTests = parseUnits("2000000"); + const oTokenBufferForTests = toUnitAmount( + getScenarioConfig().smallPoolShare.oTokenBuffer + ); const oTokenToPool = oTokenBalance.sub(oTokenBufferForTests); expect(oTokenToPool).to.gt(0); @@ -852,7 +1164,10 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { const { oToken, amoStrategy, assetToken } = fixture; // Swap OToken into the pool and asset token out. - await poolSwapTokensIn(oToken, parseUnits("1005000")); + await poolSwapTokensIn( + oToken, + toUnitAmount(getScenarioConfig().smallPoolShare.stressSwapOToken) + ); await logSnapData(await snapData(), "\nAfter swapping OToken into the pool"); expect( @@ -861,7 +1176,10 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { ).to.withinRange(dataBefore.stratBalance, dataBefore.stratBalance.add(1)); // Swap asset token into the pool and OToken out. - await poolSwapTokensIn(assetToken, parseUnits("2000000")); + await poolSwapTokensIn( + assetToken, + toUnitAmount(getScenarioConfig().smallPoolShare.stressSwapAsset) + ); await logSnapData( await snapData(), "\nAfter swapping asset token into the pool" @@ -877,7 +1195,10 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { const { amoStrategy, oToken, assetToken } = fixture; // Swap asset token into the pool and OToken out. - await poolSwapTokensIn(assetToken, parseUnits("1006000")); + await poolSwapTokensIn( + assetToken, + toUnitAmount(getScenarioConfig().smallPoolShare.stressSwapAssetAlt) + ); await logSnapData( await snapData(), "\nAfter swapping asset token into the pool" @@ -889,7 +1210,10 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { ).to.withinRange(dataBefore.stratBalance, dataBefore.stratBalance.add(1)); // Swap OToken into the pool and asset token out. - await poolSwapTokensIn(oToken, parseUnits("1005000")); + await poolSwapTokensIn( + oToken, + toUnitAmount(getScenarioConfig().smallPoolShare.stressSwapOToken) + ); await logSnapData(await snapData(), "\nAfter swapping OToken into the pool"); expect( @@ -973,11 +1297,16 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { }); it("Should fail to swap OToken to the pool", async () => { + if (fixture.skipAdvancedRebalanceTests) { + return; + } const { amoStrategy, strategist } = fixture; const tx = amoStrategy .connect(strategist) - .swapOTokensToPool(parseUnits("10")); + .swapOTokensToPool( + toUnitAmount(getScenarioConfig().insolvent.swapOTokensToPool) + ); await expect(tx).to.be.revertedWith("Protocol insolvent"); }); @@ -1326,12 +1655,23 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { ).to.equal(0); } + async function ensureVaultHasAssets(requiredAmount) { + const { vault, assetToken } = fixture; + const vaultBalance = await assetToken.balanceOf(vault.address); + if (vaultBalance.gte(requiredAmount)) { + return; + } + await setERC20TokenBalance(vault.address, assetToken, requiredAmount); + } + async function assertFailedDeposit(assetDepositAmount, errorMessage) { const { assetToken, vaultSigner, amoStrategy } = fixture; const dataBefore = await snapData(); await logSnapData(dataBefore, "\nBefore depositing asset token to strategy"); + await ensureVaultHasAssets(assetDepositAmount); + // Vault transfers wS to strategy await assetToken .connect(vaultSigner) @@ -1351,6 +1691,8 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { const dataBefore = await snapData(); await logSnapData(dataBefore, "\nBefore depositing all wS to strategy"); + await ensureVaultHasAssets(assetDepositAmount); + // Vault transfers wS to strategy await assetToken .connect(vaultSigner) diff --git a/contracts/test/strategies/oeth-supernova-amo.mainnet.fork-test.js b/contracts/test/strategies/oeth-supernova-amo.mainnet.fork-test.js new file mode 100644 index 0000000000..0442fb3b52 --- /dev/null +++ b/contracts/test/strategies/oeth-supernova-amo.mainnet.fork-test.js @@ -0,0 +1,119 @@ +const { supernovaOETHAMOFixure, createFixtureLoader } = require("../_fixture"); +const addresses = require("../../utils/addresses"); +const { shouldBehaveLikeAlgebraAmoStrategy } = require("../behaviour/algebraAmoStrategy"); + +describe.only("Mainnet Fork Test: OETH Supernova AMO Strategy", function () { + shouldBehaveLikeAlgebraAmoStrategy(async () => { + const scenarioConfig = { + attackerFrontRun: { + moderateAssetIn: "2", + largeAssetIn: "100", + largeOTokenIn: "100", + }, + poolImbalance: { + lotMoreOToken: { addOToken: 20 }, + littleMoreOToken: { addOToken: 2 }, + lotMoreAsset: { addAsset: 20 }, + littleMoreAsset: { addAsset: 2 }, + }, + smallPoolShare: { + bootstrapAssetSwapIn: "100", + bigLiquidityAsset: "50", + oTokenBuffer: "100", + stressSwapOToken: "30", + stressSwapAsset: "50", + stressSwapAssetAlt: "30", + }, + rebalanceProbe: { + frontRun: { + depositAmount: "200", + failedDepositAmount: "200", + failedDepositAllAmount: "200", + assetTiltWithdrawAmount: "40", + oTokenTiltWithdrawAmount: "20", + }, + lotMoreOToken: { + failedDepositAmount: "200", + partialWithdrawAmount: "40", + smallSwapAssetsToPool: "0.3", + largeSwapAssetsToPool: "30", + nearMaxSwapAssetsToPool: "44", + excessiveSwapAssetsToPool: "2000", + disallowedSwapOTokensToPool: "0.0001", + }, + littleMoreOToken: { + depositAmount: "120", + partialWithdrawAmount: "10", + smallSwapAssetsToPool: "0.3", + excessiveSwapAssetsToPool: "50", + disallowedSwapOTokensToPool: "0.0001", + }, + lotMoreAsset: { + failedDepositAmount: "60", + partialWithdrawAmount: "10", + smallSwapOTokensToPool: "0.03", + largeSwapOTokensToPool: "50", + overshootSwapOTokensToPool: "999", + disallowedSwapAssetsToPool: "0.00001", + }, + littleMoreAsset: { + depositAmount: "180", + partialWithdrawAmount: "10", + smallSwapOTokensToPool: "0.8", + overshootSwapOTokensToPool: "110", + disallowedSwapAssetsToPool: "0.00001", + }, + }, + insolvent: { + swapOTokensToPool: "0.1", + }, + fixtureSetup: { + imbalanceBalancePool: false, + }, + }; + + return { + skipHarvesterTest: true, + scenarioConfig, + loadFixture: async ({ + assetMintAmount = 0, + depositToStrategy = false, + balancePool = false, + poolAddAssetAmount = 0, + poolAddOTokenAmount = 0, + } = {}) => { + const fixtureLoader = await createFixtureLoader(supernovaOETHAMOFixure, { + assetMintAmount, + depositToStrategy, + balancePool, + poolAddAssetAmount, + poolAddOTokenAmount, + }); + + const fixture = await fixtureLoader(); + const oTokenPoolIndex = + (await fixture.supernovaPool.token0()) === fixture.oeth.address ? 0 : 1; + + return { + addresses: addresses.mainnet, + assetToken: fixture.weth, + oToken: fixture.oeth, + rewardToken: fixture.supernovaRewardToken, + amoStrategy: fixture.supernovaAMOStrategy, + pool: fixture.supernovaPool, + gauge: fixture.supernovaGauge, + governor: fixture.timelock, + timelock: fixture.timelock, + strategist: fixture.strategist, + nick: fixture.josh, + oTokenPoolIndex, + vaultSigner: fixture.oethVaultSigner, + vault: fixture.oethVault, + harvester: fixture.oethHarvester, + skipHarvesterTest: true, + scenarioConfig, + }; + }, + }; + }); +}); diff --git a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js index 07b579aa82..9722f4a138 100644 --- a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js +++ b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js @@ -3,9 +3,75 @@ const addresses = require("../../../utils/addresses"); const { shouldBehaveLikeAlgebraAmoStrategy } = require("../../behaviour/algebraAmoStrategy"); const { createFixtureLoader } = require("../../_fixture"); -describe("Sonic Fork Test: SwapX AMO Strategy", function () { +describe.only("Sonic Fork Test: SwapX AMO Strategy", function () { shouldBehaveLikeAlgebraAmoStrategy(async () => { + const scenarioConfig = { + attackerFrontRun: { + moderateAssetIn: "20000", + largeAssetIn: "10000000", + largeOTokenIn: "10000000", + }, + poolImbalance: { + lotMoreOToken: { addOToken: 1000000 }, + littleMoreOToken: { addOToken: 5000 }, + lotMoreAsset: { addAsset: 2000000 }, + littleMoreAsset: { addAsset: 20000 }, + }, + smallPoolShare: { + bootstrapAssetSwapIn: "10000000", + bigLiquidityAsset: "1000000", + oTokenBuffer: "2000000", + stressSwapOToken: "1005000", + stressSwapAsset: "2000000", + stressSwapAssetAlt: "1006000", + }, + rebalanceProbe: { + frontRun: { + depositAmount: "200000", + failedDepositAmount: "5000", + failedDepositAllAmount: "5000", + assetTiltWithdrawAmount: "4000", + oTokenTiltWithdrawAmount: "200", + }, + lotMoreOToken: { + failedDepositAmount: "5000", + partialWithdrawAmount: "4000", + smallSwapAssetsToPool: "3", + largeSwapAssetsToPool: "3000", + nearMaxSwapAssetsToPool: "4400", + excessiveSwapAssetsToPool: "2000000", + disallowedSwapOTokensToPool: "0.001", + }, + littleMoreOToken: { + depositAmount: "12000", + partialWithdrawAmount: "1000", + smallSwapAssetsToPool: "3", + excessiveSwapAssetsToPool: "5000", + disallowedSwapOTokensToPool: "0.001", + }, + lotMoreAsset: { + failedDepositAmount: "6000", + partialWithdrawAmount: "1000", + smallSwapOTokensToPool: "0.3", + largeSwapOTokensToPool: "5000", + overshootSwapOTokensToPool: "999990", + disallowedSwapAssetsToPool: "0.0001", + }, + littleMoreAsset: { + depositAmount: "18000", + partialWithdrawAmount: "1000", + smallSwapOTokensToPool: "8", + overshootSwapOTokensToPool: "11000", + disallowedSwapAssetsToPool: "0.0001", + }, + }, + insolvent: { + swapOTokensToPool: "10", + }, + }; + return { + scenarioConfig, loadFixture: async ({ assetMintAmount = 0, depositToStrategy = false, @@ -41,6 +107,7 @@ describe("Sonic Fork Test: SwapX AMO Strategy", function () { vaultSigner: fixture.oSonicVaultSigner, // address of the vault signer vault: fixture.oSonicVault, // address of the vault harvester: fixture.harvester, // address of the harvester + scenarioConfig, }; }, }; From d167231fc372e10c88004f58a7abe93c472bd0c5 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 20 Feb 2026 01:39:25 +0000 Subject: [PATCH 14/44] fix 6 more fork tests --- .../test/behaviour/algebraAmoStrategy.js | 41 +++++-------------- .../oeth-supernova-amo.mainnet.fork-test.js | 9 ++-- .../sonic/swapx-amo.sonic.fork-test.js | 1 + 3 files changed, 16 insertions(+), 35 deletions(-) diff --git a/contracts/test/behaviour/algebraAmoStrategy.js b/contracts/test/behaviour/algebraAmoStrategy.js index 61f08c6a38..dd8ffec94a 100644 --- a/contracts/test/behaviour/algebraAmoStrategy.js +++ b/contracts/test/behaviour/algebraAmoStrategy.js @@ -168,7 +168,7 @@ const toUnitAmount = (value) => })); */ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { - describe("ForkTest: Algebra AMO Strategy", async function () { + describe.only("ForkTest: Algebra AMO Strategy", async function () { // Retry up to 3 times on CI this.retries(isCI ? 3 : 0); let fixture, context; @@ -487,9 +487,6 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { expect(rewardTokenBalanceAfter).to.gt(rewardTokenBalanceBefore); }); it("Attacker front-run deposit within range by adding asset token to the pool", async function () { - if (fixture.skipAdvancedRebalanceTests) { - this.skip(); - } const { nick, oToken, vaultSigner, amoStrategy, assetToken } = fixture; @@ -569,10 +566,10 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { let oTokenAmountOut; beforeEach(async function () { context = await contextFunction(); - if (context.skipAdvancedRebalanceTests) { - this.skip(); - } - fixture = await context.loadFixture(); + fixture = await context.loadFixture({ + assetMintAmount: getScenarioConfig().rebalanceProbe.frontRun.tiltSeedWithdrawAmount, + depositToStrategy: true + }); const { nick, assetToken } = fixture; attackerAssetBalanceBefore = await assetToken.balanceOf(nick.address); @@ -626,7 +623,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { dataBeforeWithdraw, `\nBefore strategist withdraw ${formatUnits(withdrawAmount)} asset token` ); - + const tx = await amoStrategy .connect(vaultSigner) .withdraw(vault.address, assetToken.address, withdrawAmount); @@ -672,10 +669,10 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { let assetAmountOut; beforeEach(async function () { context = await contextFunction(); - if (context.skipAdvancedRebalanceTests) { - this.skip(); - } - fixture = await context.loadFixture(); + fixture = await context.loadFixture({ + assetMintAmount: getScenarioConfig().rebalanceProbe.frontRun.tiltSeedWithdrawAmount, + depositToStrategy: true + }); const { nick, oToken, vault, assetToken } = fixture; const oTokenAmountIn = toUnitAmount( @@ -783,9 +780,6 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { describe("with a lot more OToken in the pool", () => { beforeEach(async function () { context = await contextFunction(); - if (context.skipAdvancedRebalanceTests) { - this.skip(); - } fixture = await context.loadFixture({ assetMintAmount: 5000, depositToStrategy: true, @@ -876,9 +870,6 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { describe("with a little more OToken in the pool", () => { beforeEach(async function () { context = await contextFunction(); - if (context.skipAdvancedRebalanceTests) { - this.skip(); - } fixture = await context.loadFixture({ assetMintAmount: 20000, depositToStrategy: true, @@ -961,9 +952,6 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { describe("with a lot more asset token in the pool", () => { beforeEach(async function () { context = await contextFunction(); - if (context.skipAdvancedRebalanceTests) { - this.skip(); - } fixture = await context.loadFixture({ assetMintAmount: 5000, depositToStrategy: true, @@ -1042,9 +1030,6 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { describe("with a little more asset token in the pool", () => { beforeEach(async function () { context = await contextFunction(); - if (context.skipAdvancedRebalanceTests) { - this.skip(); - } fixture = await context.loadFixture({ assetMintAmount: 20000, depositToStrategy: true, @@ -1124,9 +1109,6 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { beforeEach(async function () { context = await contextFunction(); - if (context.skipAdvancedRebalanceTests) { - this.skip(); - } fixture = await context.loadFixture({ assetMintAmount: 5000, depositToStrategy: true, @@ -1297,9 +1279,6 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { }); it("Should fail to swap OToken to the pool", async () => { - if (fixture.skipAdvancedRebalanceTests) { - return; - } const { amoStrategy, strategist } = fixture; const tx = amoStrategy diff --git a/contracts/test/strategies/oeth-supernova-amo.mainnet.fork-test.js b/contracts/test/strategies/oeth-supernova-amo.mainnet.fork-test.js index 0442fb3b52..0e3e674baa 100644 --- a/contracts/test/strategies/oeth-supernova-amo.mainnet.fork-test.js +++ b/contracts/test/strategies/oeth-supernova-amo.mainnet.fork-test.js @@ -6,9 +6,9 @@ describe.only("Mainnet Fork Test: OETH Supernova AMO Strategy", function () { shouldBehaveLikeAlgebraAmoStrategy(async () => { const scenarioConfig = { attackerFrontRun: { - moderateAssetIn: "2", - largeAssetIn: "100", - largeOTokenIn: "100", + moderateAssetIn: "20", + largeAssetIn: "10000", + largeOTokenIn: "10000", }, poolImbalance: { lotMoreOToken: { addOToken: 20 }, @@ -29,8 +29,9 @@ describe.only("Mainnet Fork Test: OETH Supernova AMO Strategy", function () { depositAmount: "200", failedDepositAmount: "200", failedDepositAllAmount: "200", + tiltSeedWithdrawAmount: "60", assetTiltWithdrawAmount: "40", - oTokenTiltWithdrawAmount: "20", + oTokenTiltWithdrawAmount: "0.01", }, lotMoreOToken: { failedDepositAmount: "200", diff --git a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js index 9722f4a138..e44e95fd42 100644 --- a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js +++ b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js @@ -30,6 +30,7 @@ describe.only("Sonic Fork Test: SwapX AMO Strategy", function () { depositAmount: "200000", failedDepositAmount: "5000", failedDepositAllAmount: "5000", + tiltSeedWithdrawAmount: "6000", assetTiltWithdrawAmount: "4000", oTokenTiltWithdrawAmount: "200", }, From 8566cf9b3d46a4fc580ba0f6cac5ae65db75f35b Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 20 Feb 2026 01:39:55 +0000 Subject: [PATCH 15/44] remove only --- .../test/strategies/oeth-supernova-amo.mainnet.fork-test.js | 2 +- contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/test/strategies/oeth-supernova-amo.mainnet.fork-test.js b/contracts/test/strategies/oeth-supernova-amo.mainnet.fork-test.js index 0e3e674baa..7cb29c45ef 100644 --- a/contracts/test/strategies/oeth-supernova-amo.mainnet.fork-test.js +++ b/contracts/test/strategies/oeth-supernova-amo.mainnet.fork-test.js @@ -2,7 +2,7 @@ const { supernovaOETHAMOFixure, createFixtureLoader } = require("../_fixture"); const addresses = require("../../utils/addresses"); const { shouldBehaveLikeAlgebraAmoStrategy } = require("../behaviour/algebraAmoStrategy"); -describe.only("Mainnet Fork Test: OETH Supernova AMO Strategy", function () { +describe("Mainnet Fork Test: OETH Supernova AMO Strategy", function () { shouldBehaveLikeAlgebraAmoStrategy(async () => { const scenarioConfig = { attackerFrontRun: { diff --git a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js index e44e95fd42..8047cc02b0 100644 --- a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js +++ b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js @@ -3,7 +3,7 @@ const addresses = require("../../../utils/addresses"); const { shouldBehaveLikeAlgebraAmoStrategy } = require("../../behaviour/algebraAmoStrategy"); const { createFixtureLoader } = require("../../_fixture"); -describe.only("Sonic Fork Test: SwapX AMO Strategy", function () { +describe("Sonic Fork Test: SwapX AMO Strategy", function () { shouldBehaveLikeAlgebraAmoStrategy(async () => { const scenarioConfig = { attackerFrontRun: { From 6ceb0c1e6d196b871c26efae745a5dec3e428937 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 20 Feb 2026 01:55:03 +0000 Subject: [PATCH 16/44] fix some more fork tests --- contracts/test/_fixture.js | 23 +++++++++++-------- .../oeth-supernova-amo.mainnet.fork-test.js | 8 +++---- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 75261399d8..71dd11894e 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -2572,8 +2572,8 @@ async function supernovaOETHAMOFixure( assetMintAmount: 0, depositToStrategy: false, balancePool: false, - poolAddAssetAmount: 0, - poolAddOTokenAmount: 0, + poolAddWethAmount: 0, + poolAddOethAmount: 0, } ) { const fixture = await defaultFixture(); @@ -2587,8 +2587,8 @@ async function supernovaOETHAMOFixure( assetMintAmount: config?.assetMintAmount || 0, depositToStrategy: config?.depositToStrategy || false, balancePool: config?.balancePool || false, - poolAddAssetAmount: config?.poolAddAssetAmount || 0, - poolAddOTokenAmount: config?.poolAddOTokenAmount || 0, + poolAddWethAmount: config?.poolAddWethAmount || 0, + poolAddOethAmount: config?.poolAddOethAmount || 0, }; const cOETHSupernovaAMOProxy = await ethers.getContract("OETHSupernovaAMOProxy"); @@ -2690,21 +2690,24 @@ async function supernovaOETHAMOFixure( ); if (diff > 0) { - cfg.poolAddOTokenAmount += diff; + cfg.poolAddOethAmount += diff; } else if (diff < 0) { - cfg.poolAddAssetAmount += -diff; + cfg.poolAddWethAmount += -diff; } } // Add WETH to the pool directly. - if (cfg.poolAddAssetAmount > 0) { - const wethAmount = parseUnits(cfg.poolAddAssetAmount.toString(), 18); + if (cfg.poolAddWethAmount > 0) { + log(`Adding ${config.poolAddOethAmount} WETH to the pool`); + const wethAmount = parseUnits(cfg.poolAddWethAmount.toString(), 18); await weth.connect(josh).transfer(supernovaPool.address, wethAmount); } // Add OETH to the pool directly. - if (cfg.poolAddOTokenAmount > 0) { - const oethAmount = parseUnits(cfg.poolAddOTokenAmount.toString(), 18); + if (cfg.poolAddOethAmount > 0) { + log(`Adding ${config.poolAddOethAmount} OETH to the pool`); + const oethAmount = parseUnits(cfg.poolAddOethAmount.toString(), 18); + await weth.connect(josh).approve(oethVault.address, oethAmount); await oethVault.connect(josh).mint(weth.address, oethAmount, 0); await oeth.connect(josh).transfer(supernovaPool.address, oethAmount); } diff --git a/contracts/test/strategies/oeth-supernova-amo.mainnet.fork-test.js b/contracts/test/strategies/oeth-supernova-amo.mainnet.fork-test.js index 7cb29c45ef..cfe8069d64 100644 --- a/contracts/test/strategies/oeth-supernova-amo.mainnet.fork-test.js +++ b/contracts/test/strategies/oeth-supernova-amo.mainnet.fork-test.js @@ -11,9 +11,9 @@ describe("Mainnet Fork Test: OETH Supernova AMO Strategy", function () { largeOTokenIn: "10000", }, poolImbalance: { - lotMoreOToken: { addOToken: 20 }, + lotMoreOToken: { addOToken: 400 }, littleMoreOToken: { addOToken: 2 }, - lotMoreAsset: { addAsset: 20 }, + lotMoreAsset: { addAsset: 400 }, littleMoreAsset: { addAsset: 2 }, }, smallPoolShare: { @@ -87,8 +87,8 @@ describe("Mainnet Fork Test: OETH Supernova AMO Strategy", function () { assetMintAmount, depositToStrategy, balancePool, - poolAddAssetAmount, - poolAddOTokenAmount, + poolAddWethAmount: poolAddAssetAmount, + poolAddOethAmount: poolAddOTokenAmount, }); const fixture = await fixtureLoader(); From feddfd764611ce74720eab9b586b4ab3eb9951a6 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 20 Feb 2026 02:23:45 +0000 Subject: [PATCH 17/44] fix the swap contract code issue and ~10 fork test as a side effect --- .../algebra/StableSwapAMMStrategy.sol | 24 ++++++++++++++++--- .../test/behaviour/algebraAmoStrategy.js | 5 ++-- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol b/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol index fc3b99c02e..ee2d2dfb34 100644 --- a/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol +++ b/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol @@ -708,10 +708,28 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy { "Unsupported swap" ); + uint256 amount0; + uint256 amount1; + // Work out the correct order of the amounts for the pool - (uint256 amount0, uint256 amount1) = _tokenIn == asset - ? (uint256(0), amountOut) - : (amountOut, 0); + if (_tokenIn == asset) { + if (oTokenPoolIndex == 0) { + amount0 = amountOut; + amount1 = 0; + } else { + amount0 = 0; + amount1 = amountOut; + } + } else { + if (oTokenPoolIndex == 0) { + amount0 = 0; + amount1 = amountOut; + } else { + amount0 = amountOut; + amount1 = 0; + + } + } // Perform the swap on the pool IPair(pool).swap(amount0, amount1, address(this), new bytes(0)); diff --git a/contracts/test/behaviour/algebraAmoStrategy.js b/contracts/test/behaviour/algebraAmoStrategy.js index dd8ffec94a..5595156739 100644 --- a/contracts/test/behaviour/algebraAmoStrategy.js +++ b/contracts/test/behaviour/algebraAmoStrategy.js @@ -679,7 +679,8 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { getScenarioConfig().attackerFrontRun.largeOTokenIn ); - // Mint OToken using asset token + // Mint OToken using asset token + await assetToken.connect(nick).approve(vault.address, oTokenAmountIn); await vault.connect(nick).mint(assetToken.address, oTokenAmountIn, 0); attackerBalanceBefore.oToken = await oToken.balanceOf(nick.address); @@ -1813,7 +1814,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { const dataBefore = await snapData(); await logSnapData( dataBefore, - `Before swapping ${formatUnits(assetAmount)} wS into the pool` + `Before swapping ${formatUnits(assetAmount)} asset token into the pool` ); const { lpBurnAmount: expectedLpBurnAmount, oTokenBurnAmount: oTokenBurnAmount1 } = From 4affe8733847f152d3debd2228efa4161c9f4425 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 20 Feb 2026 02:29:38 +0000 Subject: [PATCH 18/44] test cleanup --- contracts/test/behaviour/algebraAmoStrategy.js | 13 +++++-------- .../oeth-supernova-amo.mainnet.fork-test.js | 3 --- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/contracts/test/behaviour/algebraAmoStrategy.js b/contracts/test/behaviour/algebraAmoStrategy.js index 5595156739..77218b9228 100644 --- a/contracts/test/behaviour/algebraAmoStrategy.js +++ b/contracts/test/behaviour/algebraAmoStrategy.js @@ -69,10 +69,7 @@ const defaultScenarioConfig = { }, insolvent: { swapOTokensToPool: "10", - }, - fixtureSetup: { - imbalanceBalancePool: true, - }, + } }; const mergeScenarioConfig = (contextConfig = {}, fixtureConfig = {}) => ({ @@ -784,7 +781,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { fixture = await context.loadFixture({ assetMintAmount: 5000, depositToStrategy: true, - balancePool: getScenarioConfig().fixtureSetup.imbalanceBalancePool, + balancePool: true, poolAddOTokenAmount: getScenarioConfig().poolImbalance.lotMoreOToken.addOToken, }); @@ -874,7 +871,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { fixture = await context.loadFixture({ assetMintAmount: 20000, depositToStrategy: true, - balancePool: getScenarioConfig().fixtureSetup.imbalanceBalancePool, + balancePool: true, poolAddOTokenAmount: getScenarioConfig().poolImbalance.littleMoreOToken.addOToken, }); @@ -956,7 +953,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { fixture = await context.loadFixture({ assetMintAmount: 5000, depositToStrategy: true, - balancePool: getScenarioConfig().fixtureSetup.imbalanceBalancePool, + balancePool: true, poolAddAssetAmount: getScenarioConfig().poolImbalance.lotMoreAsset.addAsset, }); @@ -1034,7 +1031,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { fixture = await context.loadFixture({ assetMintAmount: 20000, depositToStrategy: true, - balancePool: getScenarioConfig().fixtureSetup.imbalanceBalancePool, + balancePool: true, poolAddAssetAmount: getScenarioConfig().poolImbalance.littleMoreAsset.addAsset, }); diff --git a/contracts/test/strategies/oeth-supernova-amo.mainnet.fork-test.js b/contracts/test/strategies/oeth-supernova-amo.mainnet.fork-test.js index cfe8069d64..36a518e8a9 100644 --- a/contracts/test/strategies/oeth-supernova-amo.mainnet.fork-test.js +++ b/contracts/test/strategies/oeth-supernova-amo.mainnet.fork-test.js @@ -68,9 +68,6 @@ describe("Mainnet Fork Test: OETH Supernova AMO Strategy", function () { insolvent: { swapOTokensToPool: "0.1", }, - fixtureSetup: { - imbalanceBalancePool: false, - }, }; return { From ff4edb8e4e85385cf886f6a2a70e2d53976a67a4 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 20 Feb 2026 03:18:42 +0000 Subject: [PATCH 19/44] moving additional values to configuration field --- .../algebra/StableSwapAMMStrategy.sol | 3 +- contracts/test/_fixture.js | 2 +- .../test/behaviour/algebraAmoStrategy.js | 66 +++++++++++++------ .../oeth-supernova-amo.mainnet.fork-test.js | 11 ++++ .../sonic/swapx-amo.sonic.fork-test.js | 11 ++++ 5 files changed, 71 insertions(+), 22 deletions(-) diff --git a/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol b/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol index ee2d2dfb34..bf0020e595 100644 --- a/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol +++ b/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol @@ -726,8 +726,7 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy { amount1 = amountOut; } else { amount0 = amountOut; - amount1 = 0; - + amount1 = 0; } } diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 71dd11894e..aa6dc30358 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -2620,7 +2620,7 @@ async function supernovaOETHAMOFixure( // Supernova deployment creates a fresh empty pool, seed it once for AMO tests. if ((await supernovaPool.totalSupply()).eq(0)) { - const seedAmount = parseUnits("2000"); + const seedAmount = parseUnits("150"); await oethVault.connect(josh).mint(weth.address, seedAmount.mul(2), 0); await weth.connect(josh).transfer(supernovaPool.address, seedAmount); await oeth.connect(josh).transfer(supernovaPool.address, seedAmount); diff --git a/contracts/test/behaviour/algebraAmoStrategy.js b/contracts/test/behaviour/algebraAmoStrategy.js index 77218b9228..8c9e70aa5e 100644 --- a/contracts/test/behaviour/algebraAmoStrategy.js +++ b/contracts/test/behaviour/algebraAmoStrategy.js @@ -19,6 +19,17 @@ const defaultScenarioConfig = { lotMoreAsset: { addAsset: 2000000 }, littleMoreAsset: { addAsset: 20000 }, }, + bootstrapPool:{ + smallAssetBootstrapIn: "5000", + mediumAssetBootstrapIn: "20000", + largeAssetBootstrapIn: "5000000", + }, + mintValues:{ + extraSmall: "0.1", + extraSmallPlus: "0.2", + small: "1", + medium: "2", + }, smallPoolShare: { bootstrapAssetSwapIn: "10000000", bigLiquidityAsset: "1000000", @@ -78,6 +89,16 @@ const mergeScenarioConfig = (contextConfig = {}, fixtureConfig = {}) => ({ ...(contextConfig.attackerFrontRun || {}), ...(fixtureConfig.attackerFrontRun || {}), }, + bootstrapPool: { + ...defaultScenarioConfig.bootstrapPool, + ...(contextConfig.bootstrapPool || {}), + ...(fixtureConfig.bootstrapPool || {}), + }, + mintValues: { + ...defaultScenarioConfig.mintValues, + ...(contextConfig.mintValues || {}), + ...(fixtureConfig.mintValues || {}), + }, poolImbalance: { lotMoreOToken: { ...defaultScenarioConfig.poolImbalance.lotMoreOToken, @@ -271,13 +292,13 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { beforeEach(async () => { context = await contextFunction(); fixture = await context.loadFixture({ - assetMintAmount: 5000000, + assetMintAmount: getScenarioConfig().bootstrapPool.largeAssetBootstrapIn, depositToStrategy: false, balancePool: true, }); }); it("Vault should deposit asset token to AMO strategy", async function () { - await assertDeposit(parseUnits("2000")); + await assertDeposit(toUnitAmount(getScenarioConfig().mintValues.small)); }); it("Only vault can deposit asset token to AMO strategy", async function () { const { @@ -289,7 +310,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { assetToken, } = fixture; - const depositAmount = parseUnits("50"); + const depositAmount = toUnitAmount(getScenarioConfig().mintValues.extraSmall); await assetToken .connect(vaultSigner) .transfer(amoStrategy.address, depositAmount); @@ -313,7 +334,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { assetToken, } = fixture; - const depositAmount = parseUnits("50"); + const depositAmount = toUnitAmount(getScenarioConfig().mintValues.extraSmall); await assetToken .connect(vaultSigner) .transfer(amoStrategy.address, depositAmount); @@ -335,13 +356,13 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { beforeEach(async () => { context = await contextFunction(); fixture = await context.loadFixture({ - assetMintAmount: 5000000, + assetMintAmount: getScenarioConfig().bootstrapPool.largeAssetBootstrapIn, depositToStrategy: true, balancePool: true, }); }); it("Vault should deposit asset token", async function () { - await assertDeposit(parseUnits("5000")); + await assertDeposit(toUnitAmount(getScenarioConfig().mintValues.medium)); }); it("Vault should be able to withdraw all", async () => { await assertWithdrawAll(); @@ -416,7 +437,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { await expect(tx).to.not.emit(amoStrategy, "Withdrawal"); }); it("Vault should be able to partially withdraw", async () => { - await assertWithdrawPartial(parseUnits("1000")); + await assertWithdrawPartial(toUnitAmount(getScenarioConfig().mintValues.small)); }); it("Only vault can withdraw asset token from AMO strategy", async function () { const { amoStrategy, vault, strategist, timelock, nick, assetToken } = @@ -425,7 +446,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { for (const signer of [strategist, timelock, nick]) { const tx = amoStrategy .connect(signer) - .withdraw(vault.address, assetToken.address, parseUnits("50")); + .withdraw(vault.address, assetToken.address, toUnitAmount(getScenarioConfig().mintValues.extraSmall)); await expect(tx).to.revertedWith("Caller is not the Vault"); } @@ -462,7 +483,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { // Send some SWPx rewards to the gauge const distributorAddress = await gauge.DISTRIBUTION(); const distributorSigner = await impersonateAndFund(distributorAddress); - const rewardAmount = parseUnits("1000"); + const rewardAmount = toUnitAmount(getScenarioConfig().mintValues.small); await setERC20TokenBalance(distributorAddress, rewardToken, rewardAmount); await gauge .connect(distributorSigner) @@ -779,7 +800,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { beforeEach(async function () { context = await contextFunction(); fixture = await context.loadFixture({ - assetMintAmount: 5000, + assetMintAmount: getScenarioConfig().bootstrapPool.smallAssetBootstrapIn, depositToStrategy: true, balancePool: true, poolAddOTokenAmount: @@ -869,7 +890,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { beforeEach(async function () { context = await contextFunction(); fixture = await context.loadFixture({ - assetMintAmount: 20000, + assetMintAmount: getScenarioConfig().bootstrapPool.mediumAssetBootstrapIn, depositToStrategy: true, balancePool: true, poolAddOTokenAmount: @@ -951,7 +972,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { beforeEach(async function () { context = await contextFunction(); fixture = await context.loadFixture({ - assetMintAmount: 5000, + assetMintAmount: getScenarioConfig().bootstrapPool.smallAssetBootstrapIn, depositToStrategy: true, balancePool: true, poolAddAssetAmount: @@ -1029,7 +1050,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { beforeEach(async function () { context = await contextFunction(); fixture = await context.loadFixture({ - assetMintAmount: 20000, + assetMintAmount: getScenarioConfig().bootstrapPool.mediumAssetBootstrapIn, depositToStrategy: true, balancePool: true, poolAddAssetAmount: @@ -1108,7 +1129,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { beforeEach(async function () { context = await contextFunction(); fixture = await context.loadFixture({ - assetMintAmount: 5000, + assetMintAmount: getScenarioConfig().bootstrapPool.smallAssetBootstrapIn, depositToStrategy: true, balancePool: true, }); @@ -1207,14 +1228,14 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { beforeEach(async () => { context = await contextFunction(); fixture = await context.loadFixture({ - assetMintAmount: 5000000, + assetMintAmount: getScenarioConfig().bootstrapPool.largeAssetBootstrapIn, depositToStrategy: false, }); const { vault, vaultSigner, amoStrategy, assetToken } = fixture; // Deposit a little to the strategy. - const littleAmount = parseUnits("100"); + const littleAmount = toUnitAmount(getScenarioConfig().mintValues.extraSmallPlus); await assetToken .connect(vaultSigner) .transfer(amoStrategy.address, littleAmount); @@ -1226,6 +1247,13 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { // Calculate a 0.21% (21 basis points) loss. const lossAmount = totalAssets.mul(21).div(10000); await assetToken.connect(vaultSigner).transfer(addresses.dead, lossAmount); + + const insolventSnapshot = await snapData(); + logSnapData( + insolventSnapshot, + `Snapshot of the protocol when it is insolvent` + ); + expect( await assetToken.balanceOf(vault.address), "Must have enough asset token in vault to make insolvent" @@ -1236,7 +1264,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { const { vaultSigner, amoStrategy, assetToken } = fixture; // Vault calls deposit on the strategy. - const depositAmount = parseUnits("10"); + const depositAmount = toUnitAmount(getScenarioConfig().mintValues.extraSmall); await assetToken .connect(vaultSigner) .transfer(amoStrategy.address, depositAmount); @@ -1253,7 +1281,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { // Vault withdraws from the strategy. const tx = amoStrategy .connect(vaultSigner) - .withdraw(vault.address, assetToken.address, parseUnits("10")); + .withdraw(vault.address, assetToken.address, toUnitAmount(getScenarioConfig().mintValues.extraSmall)); await expect(tx).to.be.revertedWith("Protocol insolvent"); }); @@ -1271,7 +1299,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { const tx = amoStrategy .connect(strategist) - .swapAssetsToPool(parseUnits("10")); + .swapAssetsToPool(toUnitAmount(getScenarioConfig().mintValues.extraSmall)); await expect(tx).to.be.revertedWith("Protocol insolvent"); }); diff --git a/contracts/test/strategies/oeth-supernova-amo.mainnet.fork-test.js b/contracts/test/strategies/oeth-supernova-amo.mainnet.fork-test.js index 36a518e8a9..4ca97e17bb 100644 --- a/contracts/test/strategies/oeth-supernova-amo.mainnet.fork-test.js +++ b/contracts/test/strategies/oeth-supernova-amo.mainnet.fork-test.js @@ -10,6 +10,17 @@ describe("Mainnet Fork Test: OETH Supernova AMO Strategy", function () { largeAssetIn: "10000", largeOTokenIn: "10000", }, + bootstrapPool:{ + smallAssetBootstrapIn: "50", + mediumAssetBootstrapIn: "200", + largeAssetBootstrapIn: "500000", + }, + mintValues:{ + extraSmall: "0.1", + extraSmallPlus: "0.2", + small: "1", + medium: "2", + }, poolImbalance: { lotMoreOToken: { addOToken: 400 }, littleMoreOToken: { addOToken: 2 }, diff --git a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js index 8047cc02b0..c114415631 100644 --- a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js +++ b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js @@ -11,6 +11,17 @@ describe("Sonic Fork Test: SwapX AMO Strategy", function () { largeAssetIn: "10000000", largeOTokenIn: "10000000", }, + bootstrapPool:{ + smallAssetBootstrapIn: "5000", + mediumAssetBootstrapIn: "20000", + largeAssetBootstrapIn: "5000000", + }, + mintValues:{ + extraSmall: "50", + extraSmallPlus: "100", + small: "2000", + medium: "5000", + }, poolImbalance: { lotMoreOToken: { addOToken: 1000000 }, littleMoreOToken: { addOToken: 5000 }, From 0ad876c54f666b04022747120c2c53fd4fff7f69 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 20 Feb 2026 03:32:49 +0000 Subject: [PATCH 20/44] fix 3 more tests --- .../test/strategies/oeth-supernova-amo.mainnet.fork-test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/test/strategies/oeth-supernova-amo.mainnet.fork-test.js b/contracts/test/strategies/oeth-supernova-amo.mainnet.fork-test.js index 4ca97e17bb..a19adb2f4c 100644 --- a/contracts/test/strategies/oeth-supernova-amo.mainnet.fork-test.js +++ b/contracts/test/strategies/oeth-supernova-amo.mainnet.fork-test.js @@ -42,7 +42,7 @@ describe("Mainnet Fork Test: OETH Supernova AMO Strategy", function () { failedDepositAllAmount: "200", tiltSeedWithdrawAmount: "60", assetTiltWithdrawAmount: "40", - oTokenTiltWithdrawAmount: "0.01", + oTokenTiltWithdrawAmount: "0.001", }, lotMoreOToken: { failedDepositAmount: "200", @@ -54,7 +54,7 @@ describe("Mainnet Fork Test: OETH Supernova AMO Strategy", function () { disallowedSwapOTokensToPool: "0.0001", }, littleMoreOToken: { - depositAmount: "120", + depositAmount: "12", partialWithdrawAmount: "10", smallSwapAssetsToPool: "0.3", excessiveSwapAssetsToPool: "50", @@ -69,7 +69,7 @@ describe("Mainnet Fork Test: OETH Supernova AMO Strategy", function () { disallowedSwapAssetsToPool: "0.00001", }, littleMoreAsset: { - depositAmount: "180", + depositAmount: "18", partialWithdrawAmount: "10", smallSwapOTokensToPool: "0.8", overshootSwapOTokensToPool: "110", From 1800d1dc6319ce22a52cbdb4ecb737d9c41fe0cf Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Sat, 21 Feb 2026 09:04:51 +0000 Subject: [PATCH 21/44] fix fork test and prettier --- .../algebra/StableSwapAMMStrategy.sol | 4 +- contracts/test/_fixture.js | 8 +- .../test/behaviour/algebraAmoStrategy.js | 850 ++++++++++-------- .../oeth-supernova-amo.mainnet.fork-test.js | 35 +- .../sonic/swapx-amo.sonic.fork-test.js | 16 +- 5 files changed, 506 insertions(+), 407 deletions(-) diff --git a/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol b/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol index bf0020e595..99e039b5e7 100644 --- a/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol +++ b/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol @@ -721,12 +721,12 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy { amount1 = amountOut; } } else { - if (oTokenPoolIndex == 0) { + if (oTokenPoolIndex == 0) { amount0 = 0; amount1 = amountOut; } else { amount0 = amountOut; - amount1 = 0; + amount1 = 0; } } diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index aa6dc30358..b7ec329b60 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -2591,7 +2591,9 @@ async function supernovaOETHAMOFixure( poolAddOethAmount: config?.poolAddOethAmount || 0, }; - const cOETHSupernovaAMOProxy = await ethers.getContract("OETHSupernovaAMOProxy"); + const cOETHSupernovaAMOProxy = await ethers.getContract( + "OETHSupernovaAMOProxy" + ); const cOETHSupernovaAMOStrategy = await ethers.getContractAt( "OETHSupernovaAMOStrategy", cOETHSupernovaAMOProxy.address @@ -2612,7 +2614,9 @@ async function supernovaOETHAMOFixure( // Impersonate the OETH Vault to call strategy deposit/withdraw methods directly in tests. const oethVaultSigner = await impersonateAndFund(oethVault.address); - const oethVaultGovernor = await impersonateAndFund(await oethVault.governor()); + const oethVaultGovernor = await impersonateAndFund( + await oethVault.governor() + ); // Ensure the test actor has enough WETH to mint OETH and manipulate pool balances. await setERC20TokenBalance(josh.address, weth, oethUnits("1000000000"), hre); diff --git a/contracts/test/behaviour/algebraAmoStrategy.js b/contracts/test/behaviour/algebraAmoStrategy.js index 8c9e70aa5e..f14c1d1be7 100644 --- a/contracts/test/behaviour/algebraAmoStrategy.js +++ b/contracts/test/behaviour/algebraAmoStrategy.js @@ -19,12 +19,12 @@ const defaultScenarioConfig = { lotMoreAsset: { addAsset: 2000000 }, littleMoreAsset: { addAsset: 20000 }, }, - bootstrapPool:{ + bootstrapPool: { smallAssetBootstrapIn: "5000", mediumAssetBootstrapIn: "20000", largeAssetBootstrapIn: "5000000", }, - mintValues:{ + mintValues: { extraSmall: "0.1", extraSmallPlus: "0.2", small: "1", @@ -80,7 +80,7 @@ const defaultScenarioConfig = { }, insolvent: { swapOTokensToPool: "10", - } + }, }; const mergeScenarioConfig = (contextConfig = {}, fixtureConfig = {}) => ({ @@ -175,6 +175,7 @@ const toUnitAmount = (value) => shouldBehaveLikeAlgebraAmoStrategy(() => ({ assetToken: addresses.sonic.wS, // address of the asset token in the pool oToken: addresses.sonic.os, // address of the oToken in the pool + rewardToken: fixture.swpx, // address of the reward token amoStrategy: strategy contract, // address of the strategy pool: pool contract gauge: addresses.sonic.SwapXWSOS.gauge, // address of the gauge @@ -182,11 +183,15 @@ const toUnitAmount = (value) => timelock: addresses.sonic.timelock, // address of the timelock strategist: addresses.sonic.strategist, // address of the strategist nick: addresses.sonic.nick, // nick's address - vaultSigner: addresses.sonic.vaultSigner, // address of the vault signer + oTokenPoolIndex, // index of the oToken in the pool + vaultSigner: fixture.oSonicVaultSigner, // address of the vault signer + vault: fixture.oSonicVault, // address of the vault + harvester: fixture.harvester, // address of the harvester + scenarioConfig // verious hardcoded values for the test })); */ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { - describe.only("ForkTest: Algebra AMO Strategy", async function () { + describe("ForkTest: Algebra AMO Strategy", async function () { // Retry up to 3 times on CI this.retries(isCI ? 3 : 0); let fixture, context; @@ -198,34 +203,27 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { fixture = await context.loadFixture(); }); it("Should have constants and immutables set", async () => { - const { amoStrategy, assetToken, oToken, pool, gauge, governor } = fixture; - + const { amoStrategy, assetToken, oToken, pool, gauge, governor } = + fixture; + expect(await amoStrategy.SOLVENCY_THRESHOLD()).to.equal( parseUnits("0.998", 18) ); expect(await amoStrategy.asset()).to.equal(assetToken.address); - expect(await amoStrategy.oToken()).to.equal( - oToken.address - ); - expect(await amoStrategy.pool()).to.equal( - pool.address - ); - expect(await amoStrategy.gauge()).to.equal( - gauge.address - ); - expect(await amoStrategy.governor()).to.equal( - governor.address - ); + expect(await amoStrategy.oToken()).to.equal(oToken.address); + expect(await amoStrategy.pool()).to.equal(pool.address); + expect(await amoStrategy.gauge()).to.equal(gauge.address); + expect(await amoStrategy.governor()).to.equal(governor.address); expect(await amoStrategy.supportsAsset(assetToken.address)).to.true; expect(await amoStrategy.maxDepeg()).to.equal(parseUnits("0.01")); }); it("Should be able to check balance", async () => { const { assetToken, nick, amoStrategy } = fixture; - + const balance = await amoStrategy.checkBalance(assetToken.address); log(`check balance ${balance}`); expect(balance).gte(0); - + // This uses a transaction to call a view function so the gas usage can be reported. const tx = await amoStrategy .connect(nick) @@ -233,66 +231,48 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { await nick.sendTransaction(tx); }); it("Only Governor can approve all tokens", async () => { - const { - timelock, - strategist, - nick, - vaultSigner, - amoStrategy, - pool, - } = fixture; - - expect(await amoStrategy.connect(timelock).isGovernor()).to.equal( - true - ); - + const { timelock, strategist, nick, vaultSigner, amoStrategy, pool } = + fixture; + + expect(await amoStrategy.connect(timelock).isGovernor()).to.equal(true); + // Timelock can approve all tokens - const tx = await amoStrategy - .connect(timelock) - .safeApproveAllTokens(); + const tx = await amoStrategy.connect(timelock).safeApproveAllTokens(); await expect(tx).to.emit(pool, "Approval"); - + for (const signer of [strategist, nick, vaultSigner]) { const tx = amoStrategy.connect(signer).safeApproveAllTokens(); await expect(tx).to.be.revertedWith("Caller is not the Governor"); } }); it("Only Governor can set the max depeg", async () => { - const { - timelock, - strategist, - nick, - vaultSigner, - amoStrategy, - } = fixture; - - expect(await amoStrategy.connect(timelock).isGovernor()).to.equal( - true - ); - + const { timelock, strategist, nick, vaultSigner, amoStrategy } = + fixture; + + expect(await amoStrategy.connect(timelock).isGovernor()).to.equal(true); + // Timelock can update const newMaxDepeg = parseUnits("0.02"); - const tx = await amoStrategy - .connect(timelock) - .setMaxDepeg(newMaxDepeg); + const tx = await amoStrategy.connect(timelock).setMaxDepeg(newMaxDepeg); await expect(tx) .to.emit(amoStrategy, "MaxDepegUpdated") .withArgs(newMaxDepeg); - + expect(await amoStrategy.maxDepeg()).to.equal(newMaxDepeg); - + for (const signer of [strategist, nick, vaultSigner]) { const tx = amoStrategy.connect(signer).setMaxDepeg(newMaxDepeg); await expect(tx).to.be.revertedWith("Caller is not the Governor"); } }); }); - + describe("with asset token in the vault", () => { beforeEach(async () => { context = await contextFunction(); fixture = await context.loadFixture({ - assetMintAmount: getScenarioConfig().bootstrapPool.largeAssetBootstrapIn, + assetMintAmount: + getScenarioConfig().bootstrapPool.largeAssetBootstrapIn, depositToStrategy: false, balancePool: true, }); @@ -309,17 +289,19 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { nick, assetToken, } = fixture; - - const depositAmount = toUnitAmount(getScenarioConfig().mintValues.extraSmall); + + const depositAmount = toUnitAmount( + getScenarioConfig().mintValues.extraSmall + ); await assetToken .connect(vaultSigner) .transfer(amoStrategy.address, depositAmount); - + for (const signer of [strategist, timelock, nick]) { const tx = amoStrategy .connect(signer) .deposit(assetToken.address, depositAmount); - + await expect(tx).to.revertedWith("Caller is not the Vault"); } }); @@ -333,133 +315,143 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { nick, assetToken, } = fixture; - - const depositAmount = toUnitAmount(getScenarioConfig().mintValues.extraSmall); + + const depositAmount = toUnitAmount( + getScenarioConfig().mintValues.extraSmall + ); await assetToken .connect(vaultSigner) .transfer(amoStrategy.address, depositAmount); - + for (const signer of [strategist, timelock, nick]) { const tx = amoStrategy.connect(signer).depositAll(); - + await expect(tx).to.revertedWith("Caller is not the Vault"); } - + const tx = await amoStrategy.connect(vaultSigner).depositAll(); await expect(tx) .to.emit(amoStrategy, "Deposit") .withNamedArgs({ _asset: assetToken.address, _pToken: pool.address }); }); }); - + describe("with the strategy having OToken and asset token in a balanced pool", () => { beforeEach(async () => { context = await contextFunction(); fixture = await context.loadFixture({ - assetMintAmount: getScenarioConfig().bootstrapPool.largeAssetBootstrapIn, + assetMintAmount: + getScenarioConfig().bootstrapPool.largeAssetBootstrapIn, depositToStrategy: true, balancePool: true, }); }); it("Vault should deposit asset token", async function () { - await assertDeposit(toUnitAmount(getScenarioConfig().mintValues.medium)); + await assertDeposit( + toUnitAmount(getScenarioConfig().mintValues.medium) + ); }); it("Vault should be able to withdraw all", async () => { await assertWithdrawAll(); }); it("Vault should be able to withdraw all in SwapX Emergency", async () => { const { amoStrategy, gauge, vaultSigner } = fixture; - + const gaugeOwner = await gauge.owner(); const ownerSigner = await impersonateAndFund(gaugeOwner); await gauge.connect(ownerSigner).activateEmergencyMode(); await assertWithdrawAll(); - + // Try again when the strategy is empty await amoStrategy.connect(vaultSigner).withdrawAll(); }); it("Should fail to deposit zero asset token", async () => { const { amoStrategy, vaultSigner, assetToken } = fixture; - + const tx = amoStrategy .connect(vaultSigner) .deposit(assetToken.address, 0); - + await expect(tx).to.be.revertedWith("Must deposit something"); }); it("Should fail to deposit oToken", async () => { const { amoStrategy, vaultSigner, oToken } = fixture; - + const tx = amoStrategy .connect(vaultSigner) .deposit(oToken.address, parseUnits("1")); - + await expect(tx).to.be.revertedWith("Unsupported asset"); }); it("Should fail to withdraw zero asset token", async () => { const { amoStrategy, vaultSigner, vault, assetToken } = fixture; - + const tx = amoStrategy .connect(vaultSigner) .withdraw(vault.address, assetToken.address, 0); - + await expect(tx).to.be.revertedWith("Must withdraw something"); }); it("Should fail to withdraw oToken", async () => { - const { amoStrategy, vaultSigner, oToken, vault } = - fixture; - + const { amoStrategy, vaultSigner, oToken, vault } = fixture; + const tx = amoStrategy .connect(vaultSigner) .withdraw(vault.address, oToken.address, parseUnits("1")); - + await expect(tx).to.be.revertedWith("Unsupported asset"); }); it("Should fail to withdraw to a user", async () => { const { amoStrategy, vaultSigner, assetToken, nick } = fixture; - + const tx = amoStrategy .connect(vaultSigner) .withdraw(nick.address, assetToken.address, parseUnits("1")); - + await expect(tx).to.be.revertedWith("Only withdraw to vault allowed"); }); it("Vault should be able to withdraw all from empty strategy", async () => { const { amoStrategy, vaultSigner } = fixture; await assertWithdrawAll(); - + // Now try again after all the assets have already been withdrawn - const tx = await amoStrategy - .connect(vaultSigner) - .withdrawAll(); - + const tx = await amoStrategy.connect(vaultSigner).withdrawAll(); + // Check emitted events await expect(tx).to.not.emit(amoStrategy, "Withdrawal"); }); it("Vault should be able to partially withdraw", async () => { - await assertWithdrawPartial(toUnitAmount(getScenarioConfig().mintValues.small)); + await assertWithdrawPartial( + toUnitAmount(getScenarioConfig().mintValues.small) + ); }); it("Only vault can withdraw asset token from AMO strategy", async function () { const { amoStrategy, vault, strategist, timelock, nick, assetToken } = fixture; - + for (const signer of [strategist, timelock, nick]) { const tx = amoStrategy .connect(signer) - .withdraw(vault.address, assetToken.address, toUnitAmount(getScenarioConfig().mintValues.extraSmall)); - + .withdraw( + vault.address, + assetToken.address, + toUnitAmount(getScenarioConfig().mintValues.extraSmall) + ); + await expect(tx).to.revertedWith("Caller is not the Vault"); } }); it("Only vault and governor can withdraw all from AMO strategy", async function () { const { amoStrategy, strategist, timelock, nick } = fixture; - + for (const signer of [strategist, nick]) { const tx = amoStrategy.connect(signer).withdrawAll(); - - await expect(tx).to.revertedWith("Caller is not the Vault or Governor"); + + await expect(tx).to.revertedWith( + "Caller is not the Vault or Governor" + ); } - + // Governor can withdraw all const tx = amoStrategy.connect(timelock).withdrawAll(); await expect(tx).to.emit(amoStrategy, "Withdrawal"); @@ -469,34 +461,36 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { this.skip(); } - const { - harvester, - nick, - amoStrategy, - gauge, - rewardToken, - strategist, - } = fixture; - - const rewardTokenBalanceBefore = await rewardToken.balanceOf(strategist.address); - + const { harvester, nick, amoStrategy, gauge, rewardToken, strategist } = + fixture; + + const rewardTokenBalanceBefore = await rewardToken.balanceOf( + strategist.address + ); + // Send some SWPx rewards to the gauge const distributorAddress = await gauge.DISTRIBUTION(); const distributorSigner = await impersonateAndFund(distributorAddress); const rewardAmount = toUnitAmount(getScenarioConfig().mintValues.small); - await setERC20TokenBalance(distributorAddress, rewardToken, rewardAmount); + await setERC20TokenBalance( + distributorAddress, + rewardToken, + rewardAmount + ); await gauge .connect(distributorSigner) .notifyRewardAmount(rewardToken.address, rewardAmount); - + // Harvest the rewards // prettier-ignore const tx = await harvester .connect(nick)["harvestAndTransfer(address)"](amoStrategy.address); - + await expect(tx).to.emit(amoStrategy, "RewardTokenCollected"); - - const rewardTokenBalanceAfter = await rewardToken.balanceOf(strategist.address); + + const rewardTokenBalanceAfter = await rewardToken.balanceOf( + strategist.address + ); log( `Rewards collected ${formatUnits( rewardTokenBalanceAfter.sub(rewardTokenBalanceBefore) @@ -505,15 +499,15 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { expect(rewardTokenBalanceAfter).to.gt(rewardTokenBalanceBefore); }); it("Attacker front-run deposit within range by adding asset token to the pool", async function () { + const { nick, oToken, vaultSigner, amoStrategy, assetToken } = fixture; - const { nick, oToken, vaultSigner, amoStrategy, assetToken } = - fixture; - - const attackerAssetTokenBalanceBefore = await assetToken.balanceOf(nick.address); + const attackerAssetTokenBalanceBefore = await assetToken.balanceOf( + nick.address + ); const assetTokenAmountIn = toUnitAmount( getScenarioConfig().attackerFrontRun.moderateAssetIn ); - + const dataBeforeSwap = await snapData(); logSnapData( dataBeforeSwap, @@ -521,15 +515,18 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { assetTokenAmountIn )} asset token into the pool for OToken` ); - + // Attacker swaps a lot of asset token for OToken in the pool // This drops the pool's asset token/OToken price and increases the OToken/asset token price - const oTokenAmountOut = await poolSwapTokensIn(assetToken, assetTokenAmountIn); - + const oTokenAmountOut = await poolSwapTokensIn( + assetToken, + assetTokenAmountIn + ); + const depositAmount = toUnitAmount( getScenarioConfig().rebalanceProbe.frontRun.depositAmount ); - + const dataBeforeDeposit = await snapData(); logSnapData( dataBeforeDeposit, @@ -537,7 +534,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { depositAmount )} asset token` ); - + // Vault deposits wS to the strategy await ensureVaultHasAssets(depositAmount); await assetToken @@ -546,7 +543,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { await amoStrategy .connect(vaultSigner) .deposit(assetToken.address, depositAmount); - + const dataAfterDeposit = await snapData(); logSnapData( dataAfterDeposit, @@ -557,10 +554,10 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { )} OToken back into the pool for asset token` ); await logProfit(dataBeforeSwap); - + // Attacker swaps the OS back for wS await poolSwapTokensIn(oToken, oTokenAmountOut); - + const dataAfterFinalSwap = await snapData(); logSnapData( dataAfterFinalSwap, @@ -569,7 +566,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { )} OToken back into the pool for asset token` ); await logProfit(dataBeforeSwap); - + const attackerWsBalanceAfter = await assetToken.balanceOf(nick.address); log( `Attacker's profit ${formatUnits( @@ -585,16 +582,18 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { beforeEach(async function () { context = await contextFunction(); fixture = await context.loadFixture({ - assetMintAmount: getScenarioConfig().rebalanceProbe.frontRun.tiltSeedWithdrawAmount, - depositToStrategy: true + assetMintAmount: + getScenarioConfig().rebalanceProbe.frontRun + .tiltSeedWithdrawAmount, + depositToStrategy: true, }); const { nick, assetToken } = fixture; - + attackerAssetBalanceBefore = await assetToken.balanceOf(nick.address); const assetTokenAmountIn = toUnitAmount( getScenarioConfig().attackerFrontRun.largeAssetIn ); - + dataBeforeSwap = await snapData(); logSnapData( dataBeforeSwap, @@ -602,10 +601,13 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { assetTokenAmountIn )} asset token into the pool for OToken` ); - + // Attacker swaps a lot of asset token for OToken in the pool // This drops the pool's asset token/OToken price and increases the OToken/asset token price - oTokenAmountOut = await poolSwapTokensIn(assetToken, assetTokenAmountIn); + oTokenAmountOut = await poolSwapTokensIn( + assetToken, + assetTokenAmountIn + ); }); it("Strategist fails to deposit to strategy", async () => { await assertFailedDeposit( @@ -624,28 +626,24 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { ); }); it("Strategist should withdraw from strategy with a profit", async () => { - const { - nick, - oToken, - vault, - vaultSigner, - amoStrategy, - assetToken, - } = fixture; + const { nick, oToken, vault, vaultSigner, amoStrategy, assetToken } = + fixture; const withdrawAmount = toUnitAmount( getScenarioConfig().rebalanceProbe.frontRun.assetTiltWithdrawAmount ); - + const dataBeforeWithdraw = await snapData(); logSnapData( dataBeforeWithdraw, - `\nBefore strategist withdraw ${formatUnits(withdrawAmount)} asset token` + `\nBefore strategist withdraw ${formatUnits( + withdrawAmount + )} asset token` ); const tx = await amoStrategy .connect(vaultSigner) .withdraw(vault.address, assetToken.address, withdrawAmount); - + const dataAfterWithdraw = await snapData(); logSnapData( dataAfterWithdraw, @@ -654,17 +652,17 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { )} OToken back into the pool for asset token` ); await logProfit(dataBeforeSwap); - + // Get how much OS was burnt const receipt = await tx.wait(); const redeemEvent = receipt.events.find( (e) => e.event === "Withdrawal" && e.args._asset === oToken.address ); log(`\nWithdraw burnt ${formatUnits(redeemEvent.args._amount)} OS`); - + // Attacker swaps the OS back for wS await poolSwapTokensIn(oToken, oTokenAmountOut); - + const dataAfterFinalSwap = await snapData(); logSnapData( dataAfterFinalSwap, @@ -672,8 +670,10 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { ); const profit = await logProfit(dataBeforeSwap); expect(profit, "vault profit").to.gt(0); - - const attackerWsBalanceAfter = await assetToken.balanceOf(nick.address); + + const attackerWsBalanceAfter = await assetToken.balanceOf( + nick.address + ); log( `Attacker's profit/loss ${formatUnits( attackerWsBalanceAfter.sub(attackerAssetBalanceBefore) @@ -688,22 +688,26 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { beforeEach(async function () { context = await contextFunction(); fixture = await context.loadFixture({ - assetMintAmount: getScenarioConfig().rebalanceProbe.frontRun.tiltSeedWithdrawAmount, - depositToStrategy: true + assetMintAmount: + getScenarioConfig().rebalanceProbe.frontRun + .tiltSeedWithdrawAmount, + depositToStrategy: true, }); const { nick, oToken, vault, assetToken } = fixture; - + const oTokenAmountIn = toUnitAmount( getScenarioConfig().attackerFrontRun.largeOTokenIn ); - // Mint OToken using asset token + // Mint OToken using asset token await assetToken.connect(nick).approve(vault.address, oTokenAmountIn); await vault.connect(nick).mint(assetToken.address, oTokenAmountIn, 0); - + attackerBalanceBefore.oToken = await oToken.balanceOf(nick.address); - attackerBalanceBefore.assetToken = await assetToken.balanceOf(nick.address); - + attackerBalanceBefore.assetToken = await assetToken.balanceOf( + nick.address + ); + dataBeforeSwap = await snapData(); logSnapData( dataBeforeSwap, @@ -711,7 +715,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { oTokenAmountIn )} OToken into the pool for asset token` ); - + // Attacker swaps a lot of OToken for the asset token in the pool // This increases the pool's asset/OToken price and decreases the OToken/asset price assetAmountOut = await poolSwapTokensIn(oToken, oTokenAmountIn); @@ -733,28 +737,24 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { ); }); it("Strategist should withdraw from strategy with a profit", async () => { - const { - nick, - oToken, - vault, - vaultSigner, - amoStrategy, - assetToken, - } = fixture; + const { nick, oToken, vault, vaultSigner, amoStrategy, assetToken } = + fixture; const withdrawAmount = toUnitAmount( getScenarioConfig().rebalanceProbe.frontRun.oTokenTiltWithdrawAmount ); - + const dataBeforeWithdraw = await snapData(); logSnapData( dataBeforeWithdraw, - `\nBefore strategist withdraw ${formatUnits(withdrawAmount)} asset token` + `\nBefore strategist withdraw ${formatUnits( + withdrawAmount + )} asset token` ); - + const tx = await amoStrategy .connect(vaultSigner) .withdraw(vault.address, assetToken.address, withdrawAmount); - + const dataAfterWithdraw = await snapData(); logSnapData( dataAfterWithdraw, @@ -763,17 +763,17 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { )} asset token back into the pool for OToken` ); await logProfit(dataBeforeSwap); - + // Get how much OS was burnt const receipt = await tx.wait(); const redeemEvent = receipt.events.find( (e) => e.event === "Withdrawal" && e.args._asset === oToken.address ); log(`\nWithdraw burnt ${formatUnits(redeemEvent.args._amount)} OS`); - + // Attacker swaps the asset token back for OToken await poolSwapTokensIn(assetToken, assetAmountOut); - + const dataAfterFinalSwap = await snapData(); logSnapData( dataAfterFinalSwap, @@ -781,26 +781,31 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { ); const profit = await logProfit(dataBeforeSwap); expect(profit, "vault profit").to.gt(0); - + const attackerBalanceAfter = {}; attackerBalanceAfter.oToken = await oToken.balanceOf(nick.address); - attackerBalanceAfter.assetToken = await assetToken.balanceOf(nick.address); + attackerBalanceAfter.assetToken = await assetToken.balanceOf( + nick.address + ); log( `Attacker's profit/loss ${formatUnits( attackerBalanceAfter.oToken.sub(attackerBalanceBefore.oToken) )} OToken and ${formatUnits( - attackerBalanceAfter.assetToken.sub(attackerBalanceBefore.assetToken) + attackerBalanceAfter.assetToken.sub( + attackerBalanceBefore.assetToken + ) )} asset token` ); }); }); }); - + describe("with a lot more OToken in the pool", () => { beforeEach(async function () { context = await contextFunction(); fixture = await context.loadFixture({ - assetMintAmount: getScenarioConfig().bootstrapPool.smallAssetBootstrapIn, + assetMintAmount: + getScenarioConfig().bootstrapPool.smallAssetBootstrapIn, depositToStrategy: true, balancePool: true, poolAddOTokenAmount: @@ -821,14 +826,16 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { it("Vault should be able to partially withdraw", async () => { await assertWithdrawPartial( toUnitAmount( - getScenarioConfig().rebalanceProbe.lotMoreOToken.partialWithdrawAmount + getScenarioConfig().rebalanceProbe.lotMoreOToken + .partialWithdrawAmount ) ); }); it("Strategist should swap a little assets to the pool", async () => { await assertSwapAssetsToPool( toUnitAmount( - getScenarioConfig().rebalanceProbe.lotMoreOToken.smallSwapAssetsToPool + getScenarioConfig().rebalanceProbe.lotMoreOToken + .smallSwapAssetsToPool ) ); }); @@ -839,13 +846,14 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { const assetAmount = oTokenAmount.mul(assetReserves).div(oTokenReserves); log(`oToken amount: ${formatUnits(oTokenAmount)}`); log(`asset amount: ${formatUnits(assetAmount)}`); - + await assertSwapAssetsToPool(assetAmount); }); it("Strategist should swap a lot of assets to the pool", async () => { await assertSwapAssetsToPool( toUnitAmount( - getScenarioConfig().rebalanceProbe.lotMoreOToken.largeSwapAssetsToPool + getScenarioConfig().rebalanceProbe.lotMoreOToken + .largeSwapAssetsToPool ) ); }); @@ -853,44 +861,48 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { // TODO calculate how much asset token should be swapped to get the pool balanced await assertSwapAssetsToPool( toUnitAmount( - getScenarioConfig().rebalanceProbe.lotMoreOToken.nearMaxSwapAssetsToPool + getScenarioConfig().rebalanceProbe.lotMoreOToken + .nearMaxSwapAssetsToPool ) ); }); it("Strategist should fail to add more asset token than owned by the strategy", async () => { const { amoStrategy, strategist } = fixture; - + const tx = amoStrategy .connect(strategist) .swapAssetsToPool( toUnitAmount( - getScenarioConfig().rebalanceProbe.lotMoreOToken.excessiveSwapAssetsToPool + getScenarioConfig().rebalanceProbe.lotMoreOToken + .excessiveSwapAssetsToPool ) ); - + await expect(tx).to.be.revertedWith("Not enough LP tokens in gauge"); }); it("Strategist should fail to add more OToken to the pool", async () => { const { amoStrategy, strategist } = fixture; - + // Try swapping OToken into the pool. const tx = amoStrategy .connect(strategist) .swapOTokensToPool( toUnitAmount( - getScenarioConfig().rebalanceProbe.lotMoreOToken.disallowedSwapOTokensToPool + getScenarioConfig().rebalanceProbe.lotMoreOToken + .disallowedSwapOTokensToPool ) ); - + await expect(tx).to.be.revertedWith("OTokens balance worse"); }); }); - + describe("with a little more OToken in the pool", () => { beforeEach(async function () { context = await contextFunction(); fixture = await context.loadFixture({ - assetMintAmount: getScenarioConfig().bootstrapPool.mediumAssetBootstrapIn, + assetMintAmount: + getScenarioConfig().bootstrapPool.mediumAssetBootstrapIn, depositToStrategy: true, balancePool: true, poolAddOTokenAmount: @@ -910,14 +922,16 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { it("Vault should be able to partially withdraw", async () => { await assertWithdrawPartial( toUnitAmount( - getScenarioConfig().rebalanceProbe.littleMoreOToken.partialWithdrawAmount + getScenarioConfig().rebalanceProbe.littleMoreOToken + .partialWithdrawAmount ) ); }); it("Strategist should swap a little assets to the pool", async () => { await assertSwapAssetsToPool( toUnitAmount( - getScenarioConfig().rebalanceProbe.littleMoreOToken.smallSwapAssetsToPool + getScenarioConfig().rebalanceProbe.littleMoreOToken + .smallSwapAssetsToPool ) ); }); @@ -940,7 +954,8 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { .connect(strategist) .swapAssetsToPool( toUnitAmount( - getScenarioConfig().rebalanceProbe.littleMoreOToken.excessiveSwapAssetsToPool + getScenarioConfig().rebalanceProbe.littleMoreOToken + .excessiveSwapAssetsToPool ) ); @@ -960,7 +975,8 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { .connect(strategist) .swapOTokensToPool( toUnitAmount( - getScenarioConfig().rebalanceProbe.littleMoreOToken.disallowedSwapOTokensToPool + getScenarioConfig().rebalanceProbe.littleMoreOToken + .disallowedSwapOTokensToPool ) ); @@ -972,7 +988,8 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { beforeEach(async function () { context = await contextFunction(); fixture = await context.loadFixture({ - assetMintAmount: getScenarioConfig().bootstrapPool.smallAssetBootstrapIn, + assetMintAmount: + getScenarioConfig().bootstrapPool.smallAssetBootstrapIn, depositToStrategy: true, balancePool: true, poolAddAssetAmount: @@ -993,21 +1010,24 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { it("Vault should be able to partially withdraw", async () => { await assertWithdrawPartial( toUnitAmount( - getScenarioConfig().rebalanceProbe.lotMoreAsset.partialWithdrawAmount + getScenarioConfig().rebalanceProbe.lotMoreAsset + .partialWithdrawAmount ) ); }); it("Strategist should swap a little OToken to the pool", async () => { await assertSwapOTokensToPool( toUnitAmount( - getScenarioConfig().rebalanceProbe.lotMoreAsset.smallSwapOTokensToPool + getScenarioConfig().rebalanceProbe.lotMoreAsset + .smallSwapOTokensToPool ) ); }); it("Strategist should swap a lot of OToken to the pool", async () => { await assertSwapOTokensToPool( toUnitAmount( - getScenarioConfig().rebalanceProbe.lotMoreAsset.largeSwapOTokensToPool + getScenarioConfig().rebalanceProbe.lotMoreAsset + .largeSwapOTokensToPool ) ); }); @@ -1021,11 +1041,15 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { it("Strategist should fail to add so much OToken that it overshoots", async () => { const { amoStrategy, strategist } = fixture; + const dataBefore = await snapData(); + await logSnapData(dataBefore, "Before swapping OToken to the pool"); + const tx = amoStrategy .connect(strategist) .swapOTokensToPool( toUnitAmount( - getScenarioConfig().rebalanceProbe.lotMoreAsset.overshootSwapOTokensToPool + getScenarioConfig().rebalanceProbe.lotMoreAsset + .overshootSwapOTokensToPool ) ); @@ -1038,7 +1062,8 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { .connect(strategist) .swapAssetsToPool( toUnitAmount( - getScenarioConfig().rebalanceProbe.lotMoreAsset.disallowedSwapAssetsToPool + getScenarioConfig().rebalanceProbe.lotMoreAsset + .disallowedSwapAssetsToPool ) ); @@ -1050,7 +1075,8 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { beforeEach(async function () { context = await contextFunction(); fixture = await context.loadFixture({ - assetMintAmount: getScenarioConfig().bootstrapPool.mediumAssetBootstrapIn, + assetMintAmount: + getScenarioConfig().bootstrapPool.mediumAssetBootstrapIn, depositToStrategy: true, balancePool: true, poolAddAssetAmount: @@ -1070,14 +1096,16 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { it("Vault should be able to partially withdraw", async () => { await assertWithdrawPartial( toUnitAmount( - getScenarioConfig().rebalanceProbe.littleMoreAsset.partialWithdrawAmount + getScenarioConfig().rebalanceProbe.littleMoreAsset + .partialWithdrawAmount ) ); }); it("Strategist should swap a little OToken to the pool", async () => { await assertSwapOTokensToPool( toUnitAmount( - getScenarioConfig().rebalanceProbe.littleMoreAsset.smallSwapOTokensToPool + getScenarioConfig().rebalanceProbe.littleMoreAsset + .smallSwapOTokensToPool ) ); }); @@ -1102,7 +1130,8 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { .connect(strategist) .swapOTokensToPool( toUnitAmount( - getScenarioConfig().rebalanceProbe.littleMoreAsset.overshootSwapOTokensToPool + getScenarioConfig().rebalanceProbe.littleMoreAsset + .overshootSwapOTokensToPool ) ); @@ -1115,7 +1144,8 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { .connect(strategist) .swapAssetsToPool( toUnitAmount( - getScenarioConfig().rebalanceProbe.littleMoreAsset.disallowedSwapAssetsToPool + getScenarioConfig().rebalanceProbe.littleMoreAsset + .disallowedSwapAssetsToPool ) ); @@ -1129,7 +1159,8 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { beforeEach(async function () { context = await contextFunction(); fixture = await context.loadFixture({ - assetMintAmount: getScenarioConfig().bootstrapPool.smallAssetBootstrapIn, + assetMintAmount: + getScenarioConfig().bootstrapPool.smallAssetBootstrapIn, depositToStrategy: true, balancePool: true, }); @@ -1169,12 +1200,18 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { oToken, toUnitAmount(getScenarioConfig().smallPoolShare.stressSwapOToken) ); - await logSnapData(await snapData(), "\nAfter swapping OToken into the pool"); + await logSnapData( + await snapData(), + "\nAfter swapping OToken into the pool" + ); expect( await amoStrategy.checkBalance(assetToken.address), "Strategy's check balance" - ).to.withinRange(dataBefore.stratBalance, dataBefore.stratBalance.add(1)); + ).to.withinRange( + dataBefore.stratBalance, + dataBefore.stratBalance.add(1) + ); // Swap asset token into the pool and OToken out. await poolSwapTokensIn( @@ -1189,7 +1226,10 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { expect( await amoStrategy.checkBalance(assetToken.address), "Strategy's check balance" - ).to.withinRange(dataBefore.stratBalance, dataBefore.stratBalance.add(1)); + ).to.withinRange( + dataBefore.stratBalance, + dataBefore.stratBalance.add(1) + ); }); it("A lot of asset token is swapped into the pool", async () => { @@ -1208,19 +1248,28 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { expect( await amoStrategy.checkBalance(assetToken.address), "Strategy's check balance" - ).to.withinRange(dataBefore.stratBalance, dataBefore.stratBalance.add(1)); + ).to.withinRange( + dataBefore.stratBalance, + dataBefore.stratBalance.add(1) + ); // Swap OToken into the pool and asset token out. await poolSwapTokensIn( oToken, toUnitAmount(getScenarioConfig().smallPoolShare.stressSwapOToken) ); - await logSnapData(await snapData(), "\nAfter swapping OToken into the pool"); + await logSnapData( + await snapData(), + "\nAfter swapping OToken into the pool" + ); expect( await amoStrategy.checkBalance(assetToken.address), "Strategy's check balance" - ).to.withinRange(dataBefore.stratBalance, dataBefore.stratBalance.add(1)); + ).to.withinRange( + dataBefore.stratBalance, + dataBefore.stratBalance.add(1) + ); }); }); @@ -1228,14 +1277,17 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { beforeEach(async () => { context = await contextFunction(); fixture = await context.loadFixture({ - assetMintAmount: getScenarioConfig().bootstrapPool.largeAssetBootstrapIn, + assetMintAmount: + getScenarioConfig().bootstrapPool.largeAssetBootstrapIn, depositToStrategy: false, }); const { vault, vaultSigner, amoStrategy, assetToken } = fixture; // Deposit a little to the strategy. - const littleAmount = toUnitAmount(getScenarioConfig().mintValues.extraSmallPlus); + const littleAmount = toUnitAmount( + getScenarioConfig().mintValues.extraSmallPlus + ); await assetToken .connect(vaultSigner) .transfer(amoStrategy.address, littleAmount); @@ -1246,13 +1298,15 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { const totalAssets = await vault.totalValue(); // Calculate a 0.21% (21 basis points) loss. const lossAmount = totalAssets.mul(21).div(10000); - await assetToken.connect(vaultSigner).transfer(addresses.dead, lossAmount); + await assetToken + .connect(vaultSigner) + .transfer(addresses.dead, lossAmount); const insolventSnapshot = await snapData(); - logSnapData( - insolventSnapshot, - `Snapshot of the protocol when it is insolvent` - ); + logSnapData( + insolventSnapshot, + `Snapshot of the protocol when it is insolvent` + ); expect( await assetToken.balanceOf(vault.address), @@ -1264,7 +1318,9 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { const { vaultSigner, amoStrategy, assetToken } = fixture; // Vault calls deposit on the strategy. - const depositAmount = toUnitAmount(getScenarioConfig().mintValues.extraSmall); + const depositAmount = toUnitAmount( + getScenarioConfig().mintValues.extraSmall + ); await assetToken .connect(vaultSigner) .transfer(amoStrategy.address, depositAmount); @@ -1281,7 +1337,11 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { // Vault withdraws from the strategy. const tx = amoStrategy .connect(vaultSigner) - .withdraw(vault.address, assetToken.address, toUnitAmount(getScenarioConfig().mintValues.extraSmall)); + .withdraw( + vault.address, + assetToken.address, + toUnitAmount(getScenarioConfig().mintValues.extraSmall) + ); await expect(tx).to.be.revertedWith("Protocol insolvent"); }); @@ -1299,7 +1359,9 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { const tx = amoStrategy .connect(strategist) - .swapAssetsToPool(toUnitAmount(getScenarioConfig().mintValues.extraSmall)); + .swapAssetsToPool( + toUnitAmount(getScenarioConfig().mintValues.extraSmall) + ); await expect(tx).to.be.revertedWith("Protocol insolvent"); }); @@ -1316,7 +1378,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { await expect(tx).to.be.revertedWith("Protocol insolvent"); }); }); - + const poolSwapTokensIn = async (tokenIn, amountIn) => { const { nick, pool, assetToken, oTokenPoolIndex } = fixture; const amountOut = await pool.getAmountOut(amountIn, tokenIn.address); @@ -1325,27 +1387,27 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { if (tokenIn.address == assetToken.address) { await pool.swap( oTokenPoolIndex == 1 ? 0 : amountOut, - oTokenPoolIndex == 1 ? amountOut: 0, + oTokenPoolIndex == 1 ? amountOut : 0, nick.address, "0x" ); } else { await pool.swap( oTokenPoolIndex == 0 ? 0 : amountOut, - oTokenPoolIndex == 0 ? amountOut: 0, + oTokenPoolIndex == 0 ? amountOut : 0, nick.address, "0x" ); } - + return amountOut; }; - + const precision = parseUnits("1", 18); // Calculate the value of asset token and OToken assuming the pool is balanced const calcReserveValue = (reserves) => { const k = calcInvariant(reserves); - + // If x = y, let’s denote x = y = z (where z is the common reserve value) // Substitute z into the invariant: // k = z^3 * z + z * z^3 @@ -1357,22 +1419,22 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { const z = sqrt(zSquared.mul(precision)); return z.mul(2); }; - + const calcInvariant = (reserves) => { const x = reserves.assetToken; const y = reserves.oToken; const a = x.mul(y).div(precision); const b = x.mul(x).div(precision).add(y.mul(y).div(precision)); const k = a.mul(b).div(precision); - + return k; }; - + // Babylonian square root function for Ethers.js BigNumber function sqrt(value) { // Convert input to BigNumber if it isn't already let bn = ethers.BigNumber.from(value); - + // Handle edge cases if (bn.lt(0)) { throw new Error("Square root of negative number is not supported"); @@ -1380,39 +1442,38 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { if (bn.eq(0)) { return ethers.BigNumber.from(0); } - + // Initial guess (number / 2) let guess = bn.div(2); - + // Define precision threshold (in wei scale, 10^-18) const epsilon = ethers.BigNumber.from("1"); // 1 wei precision - + // Keep refining until we reach desired precision while (true) { // Babylonian method: nextGuess = (guess + number/guess) / 2 // Using mul and div for BigNumber arithmetic let numerator = guess.add(bn.div(guess)); let nextGuess = numerator.div(2); - + // Calculate absolute difference let diff = nextGuess.gt(guess) ? nextGuess.sub(guess) : guess.sub(nextGuess); - + // If difference is less than epsilon, we're done if (diff.lte(epsilon)) { return nextGuess; } - + // Update guess for next iteration guess = nextGuess; } } - + const snapData = async () => { - const { vault, amoStrategy, oToken, pool, gauge, assetToken } = - fixture; - + const { vault, amoStrategy, oToken, pool, gauge, assetToken } = fixture; + const stratBalance = await amoStrategy.checkBalance(assetToken.address); const oTokenSupply = await oToken.totalSupply(); const vaultAssets = await vault.totalValue(); @@ -1427,20 +1488,21 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { ); // asset/oToken price = assetToken / oToken const sellPrice = assetAmount; - + // Amount of asset token sold from buying 1 OToken - const oTokenAmount = await pool.getAmountOut(parseUnits("1"), assetToken.address); + const oTokenAmount = await pool.getAmountOut( + parseUnits("1"), + assetToken.address + ); // OToken/asset price = asset / OToken const buyPrice = parseUnits("1", 36).div(oTokenAmount); - + const k = calcInvariant(reserves); - const stratGaugeBalance = await gauge.balanceOf( - amoStrategy.address - ); + const stratGaugeBalance = await gauge.balanceOf(amoStrategy.address); const gaugeSupply = await gauge.totalSupply(); const vaultAssetBalance = await assetToken.balanceOf(vault.address); const stratAssetBalance = await assetToken.balanceOf(amoStrategy.address); - + return { stratBalance, oTokenSupply, @@ -1456,7 +1518,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { k, }; }; - + const logSnapData = async (data, message) => { const totalReserves = data.reserves.assetToken.add(data.reserves.oToken); const reserversPercentage = { @@ -1474,19 +1536,21 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { log(`Vault assets : ${formatUnits(data.vaultAssets)}`); log(`pool supply : ${formatUnits(data.poolSupply)}`); log( - `reserves assetToken : ${formatUnits(data.reserves.assetToken)} ${formatUnits( - reserversPercentage.assetToken, - 2 - )}%` + `reserves assetToken : ${formatUnits( + data.reserves.assetToken + )} ${formatUnits(reserversPercentage.assetToken, 2)}%` ); log( - `reserves OToken : ${formatUnits(data.reserves.oToken)} ${formatUnits( - reserversPercentage.oToken, - 2 - )}%` + `reserves OToken : ${formatUnits( + data.reserves.oToken + )} ${formatUnits(reserversPercentage.oToken, 2)}%` + ); + log( + `buy price : ${formatUnits(data.buyPrice)} OToken/assetToken` + ); + log( + `sell price : ${formatUnits(data.sellPrice)} OToken/assetToken` ); - log(`buy price : ${formatUnits(data.buyPrice)} OToken/assetToken`); - log(`sell price : ${formatUnits(data.sellPrice)} OToken/assetToken`); log(`Invariant K : ${formatUnits(data.k)}`); log( `strat gauge balance : ${formatUnits( @@ -1496,17 +1560,19 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { log(`gauge supply : ${formatUnits(data.gaugeSupply)}`); log(`vault asset balance : ${formatUnits(data.vaultAssetBalance)}`); }; - + const logProfit = async (dataBefore) => { const { oToken, vault, amoStrategy, assetToken } = fixture; - - const stratBalanceAfter = await amoStrategy.checkBalance(assetToken.address); + + const stratBalanceAfter = await amoStrategy.checkBalance( + assetToken.address + ); const oTokenSupplyAfter = await oToken.totalSupply(); const vaultAssetsAfter = await vault.totalValue(); const profit = vaultAssetsAfter .sub(dataBefore.vaultAssets) .add(dataBefore.oTokenSupply.sub(oTokenSupplyAfter)); - + log( `Change strat balance: ${formatUnits( stratBalanceAfter.sub(dataBefore.stratBalance) @@ -1523,46 +1589,47 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { )}` ); log(`Profit : ${formatUnits(profit)}`); - + return profit; }; - + const assertChangedData = async (dataBefore, delta) => { - const { oToken, vault, amoStrategy, gauge, assetToken } = - fixture; - + const { oToken, vault, amoStrategy, gauge, assetToken } = fixture; + if (delta.stratBalance != undefined) { const expectedStratBalance = dataBefore.stratBalance.add( delta.stratBalance ); log(`Expected strategy balance: ${formatUnits(expectedStratBalance)}`); - - expect(await amoStrategy.checkBalance(assetToken.address)).to.withinRange( + + expect( + await amoStrategy.checkBalance(assetToken.address) + ).to.withinRange( expectedStratBalance.sub(15), expectedStratBalance.add(15), "Strategy's check balance" ); } - + if (delta.oTokenSupply != undefined) { const expectedSupply = dataBefore.oTokenSupply.add(delta.oTokenSupply); expect(await oToken.totalSupply(), "oToken total supply").to.equal( expectedSupply ); } - + // Check Vault's asset token balance if (delta.vaultAssetBalance != undefined) { - expect(await assetToken.balanceOf(vault.address)).to.equal( + expect(await assetToken.balanceOf(vault.address)).to.equal( dataBefore.vaultAssetBalance.add(delta.vaultAssetBalance), "Vault's assetToken balance" ); } - + // Check the pool's reserves if (delta.reserves != undefined) { const { assetReserves, oTokenReserves } = await getPoolReserves(); - + // If the asset reserves delta is a function, call it to check the asset token reserves if (typeof delta.reserves.assetToken == "function") { // Call test function to check the asset token reserves @@ -1577,22 +1644,20 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { dataBefore.reserves.oToken.add(delta.reserves.oToken) ); } - + if (delta.stratGaugeBalance) { // Check the strategy's gauge balance const expectedStratGaugeBalance = dataBefore.stratGaugeBalance.add( delta.stratGaugeBalance ); - expect( - await gauge.balanceOf(amoStrategy.address) - ).to.withinRange( + expect(await gauge.balanceOf(amoStrategy.address)).to.withinRange( expectedStratGaugeBalance.sub(1), expectedStratGaugeBalance.add(1), "Strategy's gauge balance" ); } }; - + async function assertDeposit(assetDepositAmount) { const { nick, @@ -1606,14 +1671,14 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { await assetToken.connect(nick).approve(vault.address, assetDepositAmount); await vault.connect(nick).mint(assetToken.address, assetDepositAmount, 0); - + const dataBefore = await snapData(); await logSnapData(dataBefore, "\nBefore depositing asset to strategy"); - + const { lpMintAmount, oTokenMintAmount } = await calcOTokenMintAmount( assetDepositAmount ); - + // Vault transfers asset to strategy await assetToken .connect(vaultSigner) @@ -1622,26 +1687,30 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { const tx = await amoStrategy .connect(vaultSigner) .deposit(assetToken.address, assetDepositAmount); - + await logSnapData( await snapData(), - `\nAfter depositing ${formatUnits(assetDepositAmount)} asset to strategy` + `\nAfter depositing ${formatUnits( + assetDepositAmount + )} asset to strategy` ); await logProfit(dataBefore); - + // Check emitted events - await expect(tx).to.emit(amoStrategy, "Deposit") + await expect(tx) + .to.emit(amoStrategy, "Deposit") .withArgs(assetToken.address, pool.address, assetDepositAmount); - await expect(tx).to.emit(amoStrategy, "Deposit") + await expect(tx) + .to.emit(amoStrategy, "Deposit") .withArgs(oToken.address, pool.address, oTokenMintAmount); - + // Calculate the value of the asset token and oToken added to the pool if the pool was balanced const depositValue = calcReserveValue({ assetToken: assetDepositAmount, oToken: oTokenMintAmount, }); log(`Value of deposit: ${formatUnits(depositValue)}`); - + await assertChangedData(dataBefore, { stratBalance: depositValue, oTokenSupply: oTokenMintAmount, @@ -1649,7 +1718,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { vaultAssetBalance: assetDepositAmount.mul(-1), gaugeSupply: lpMintAmount, }); - + expect( await pool.balanceOf(amoStrategy.address), "Strategy's pool LP balance" @@ -1659,7 +1728,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { "Strategy's OToken balance" ).to.equal(0); } - + async function ensureVaultHasAssets(requiredAmount) { const { vault, assetToken } = fixture; const vaultBalance = await assetToken.balanceOf(vault.address); @@ -1671,59 +1740,64 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { async function assertFailedDeposit(assetDepositAmount, errorMessage) { const { assetToken, vaultSigner, amoStrategy } = fixture; - + const dataBefore = await snapData(); - await logSnapData(dataBefore, "\nBefore depositing asset token to strategy"); - + await logSnapData( + dataBefore, + "\nBefore depositing asset token to strategy" + ); + await ensureVaultHasAssets(assetDepositAmount); // Vault transfers wS to strategy await assetToken .connect(vaultSigner) .transfer(amoStrategy.address, assetDepositAmount); - + // Vault calls deposit on the strategy const tx = amoStrategy .connect(vaultSigner) .deposit(assetToken.address, assetDepositAmount); - + await expect(tx, "deposit to strategy").to.be.revertedWith(errorMessage); } - + async function assertFailedDepositAll(assetDepositAmount, errorMessage) { const { assetToken, vaultSigner, amoStrategy } = fixture; - + const dataBefore = await snapData(); await logSnapData(dataBefore, "\nBefore depositing all wS to strategy"); - + await ensureVaultHasAssets(assetDepositAmount); // Vault transfers wS to strategy await assetToken .connect(vaultSigner) .transfer(amoStrategy.address, assetDepositAmount); - + // Vault calls depositAll on the strategy const tx = amoStrategy.connect(vaultSigner).depositAll(); - - await expect(tx, "depositAll to strategy").to.be.revertedWith(errorMessage); + + await expect(tx, "depositAll to strategy").to.be.revertedWith( + errorMessage + ); } - + async function assertWithdrawAll() { - const { amoStrategy, pool, oToken, vaultSigner, assetToken } = - fixture; - + const { amoStrategy, pool, oToken, vaultSigner, assetToken } = fixture; + const dataBefore = await snapData(); await logSnapData(dataBefore); - - const { oTokenBurnAmount, assetTokenWithdrawAmount } = await calcWithdrawAllAmounts(); - + + const { oTokenBurnAmount, assetTokenWithdrawAmount } = + await calcWithdrawAllAmounts(); + // Now try to withdraw all the wS from the strategy const tx = await amoStrategy.connect(vaultSigner).withdrawAll(); - + await logSnapData(await snapData(), "\nAfter full withdraw"); await logProfit(dataBefore); - + // Check emitted events await expect(tx) .to.emit(amoStrategy, "Withdrawal") @@ -1731,26 +1805,29 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { await expect(tx) .to.emit(amoStrategy, "Withdrawal") .withArgs(oToken.address, pool.address, oTokenBurnAmount); - + // Calculate the value of the asset token and oToken removed from the pool if the pool was balanced const withdrawValue = calcReserveValue({ assetToken: assetTokenWithdrawAmount, oToken: oTokenBurnAmount, }); - + await assertChangedData(dataBefore, { stratBalance: withdrawValue.mul(-1), oTokenSupply: oTokenBurnAmount.mul(-1), - reserves: { assetToken: assetTokenWithdrawAmount.mul(-1), oToken: oTokenBurnAmount.mul(-1) }, + reserves: { + assetToken: assetTokenWithdrawAmount.mul(-1), + oToken: oTokenBurnAmount.mul(-1), + }, vaultAssetBalance: assetTokenWithdrawAmount, stratGaugeBalance: dataBefore.stratGaugeBalance.mul(-1), }); - + expect( assetTokenWithdrawAmount.add(oTokenBurnAmount), "asset token withdraw and oToken burn >= strategy balance" ).to.gte(dataBefore.stratBalance); - + expect( await pool.balanceOf(amoStrategy.address), "Strategy's pool LP balance" @@ -1760,34 +1837,28 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { "Strategy's oToken balance" ).to.equal(0); } - + async function assertWithdrawPartial(assetTokenWithdrawAmount) { - const { - amoStrategy, - oToken, - pool, - vault, - vaultSigner, - assetToken, - } = fixture; - + const { amoStrategy, oToken, pool, vault, vaultSigner, assetToken } = + fixture; + const dataBefore = await snapData(); - + const { lpBurnAmount, oTokenBurnAmount } = await calcOTokenWithdrawAmount( assetTokenWithdrawAmount ); - + // Now try to withdraw the asset token from the strategy const tx = await amoStrategy .connect(vaultSigner) .withdraw(vault.address, assetToken.address, assetTokenWithdrawAmount); - + await logSnapData( await snapData(), `\nAfter withdraw of ${formatUnits(assetTokenWithdrawAmount)}` ); await logProfit(dataBefore); - + // Check emitted events await expect(tx) .to.emit(amoStrategy, "Withdrawal") @@ -1796,13 +1867,13 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { _asset: oToken.address, _pToken: pool.address, }); - + // Calculate the value of the asset token and OToken removed from the pool if the pool was balanced const withdrawValue = calcReserveValue({ assetToken: assetTokenWithdrawAmount, oToken: oTokenBurnAmount, }); - + await assertChangedData(dataBefore, { stratBalance: withdrawValue.mul(-1), oTokenSupply: oTokenBurnAmount.mul(-1), @@ -1810,7 +1881,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { assetToken: (actualAssetTokenReserve) => { const expectedAssetTokenReserves = dataBefore.reserves.assetToken.sub(assetTokenWithdrawAmount); - + expect(actualAssetTokenReserve).to.withinRange( expectedAssetTokenReserves.sub(50), expectedAssetTokenReserves, @@ -1822,7 +1893,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { vaultAssetBalance: assetTokenWithdrawAmount, gaugeSupply: lpBurnAmount.mul(-1), }); - + expect( await pool.balanceOf(amoStrategy.address), "Strategy's pool LP balance" @@ -1832,30 +1903,38 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { "Strategy's OToken balance" ).to.equal(0); } - + async function assertSwapAssetsToPool(assetAmount) { const { oToken, amoStrategy, pool, strategist, assetToken } = fixture; - + const dataBefore = await snapData(); await logSnapData( dataBefore, `Before swapping ${formatUnits(assetAmount)} asset token into the pool` ); - - const { lpBurnAmount: expectedLpBurnAmount, oTokenBurnAmount: oTokenBurnAmount1 } = - await calcOTokenWithdrawAmount(assetAmount); + + const { + lpBurnAmount: expectedLpBurnAmount, + oTokenBurnAmount: oTokenBurnAmount1, + } = await calcOTokenWithdrawAmount(assetAmount); // TODO this is not accurate as the liquidity needs to be removed first - const oTokenBurnAmount2 = await pool.getAmountOut(assetAmount, assetToken.address); + const oTokenBurnAmount2 = await pool.getAmountOut( + assetAmount, + assetToken.address + ); const oTokenBurnAmount = oTokenBurnAmount1.add(oTokenBurnAmount2); - + // Swap asset token to the pool and burn the received OToken from the pool const tx = await amoStrategy .connect(strategist) .swapAssetsToPool(assetAmount); - - await logSnapData(await snapData(), "\nAfter swapping assets to the pool"); + + await logSnapData( + await snapData(), + "\nAfter swapping assets to the pool" + ); await logProfit(dataBefore); - + // Check emitted event await expect(tx).to.emittedEvent("SwapAssetsToPool", [ (actualAssetTokenAmount) => { @@ -1875,7 +1954,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { ); }, ]); - + await assertChangedData( dataBefore, { @@ -1885,7 +1964,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { }, fixture ); - + expect( await pool.balanceOf(amoStrategy.address), "Strategy's pool LP balance" @@ -1895,26 +1974,29 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { "Strategy's oToken balance" ).to.equal(0); } - + async function assertSwapOTokensToPool(oTokenAmount) { const { oToken, pool, amoStrategy, strategist } = fixture; - + const dataBefore = await snapData(); await logSnapData(dataBefore, "Before swapping OTokens to the pool"); - + // Mint OToken and swap into the pool, then mint more OToken to add with the swapped out asset token. const tx = await amoStrategy .connect(strategist) .swapOTokensToPool(oTokenAmount); - + // Check emitted event await expect(tx) .emit(amoStrategy, "SwapOTokensToPool") .withNamedArgs({ oTokenMinted: oTokenAmount }); - - await logSnapData(await snapData(), "\nAfter swapping OTokens to the pool"); + + await logSnapData( + await snapData(), + "\nAfter swapping OTokens to the pool" + ); await logProfit(dataBefore); - + await assertChangedData( dataBefore, { @@ -1924,7 +2006,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { }, fixture ); - + expect( await pool.balanceOf(amoStrategy.address), "Strategy's pool LP balance" @@ -1934,29 +2016,33 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { "Strategy's OToken balance" ).to.equal(0); } - + // Calculate the minted OS amount for a deposit async function calcOTokenMintAmount(assetDepositAmount) { const { pool } = fixture; - + const { assetReserves, oTokenReserves } = await getPoolReserves(); - - const oTokenMintAmount = assetDepositAmount.mul(oTokenReserves).div(assetReserves); + + const oTokenMintAmount = assetDepositAmount + .mul(oTokenReserves) + .div(assetReserves); log(`OToken mint amount : ${formatUnits(oTokenMintAmount)}`); - + const lpTotalSupply = await pool.totalSupply(); - const lpMintAmount = assetDepositAmount.mul(lpTotalSupply).div(assetReserves); - + const lpMintAmount = assetDepositAmount + .mul(lpTotalSupply) + .div(assetReserves); + return { lpMintAmount, oTokenMintAmount }; } - + async function getPoolReserves() { const { pool, oTokenPoolIndex } = fixture; let assetReserves, oTokenReserves; // Get the reserves of the pool const { _reserve0, _reserve1 } = await pool.getReserves(); - + assetReserves = oTokenPoolIndex === 0 ? _reserve1 : _reserve0; oTokenReserves = oTokenPoolIndex === 0 ? _reserve0 : _reserve1; @@ -1965,9 +2051,9 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { // Calculate the amount of oToken burnt from a withdraw async function calcOTokenWithdrawAmount(assetTokenWithdrawAmount) { const { pool } = fixture; - + const { assetReserves, oTokenReserves } = await getPoolReserves(); - + // lp tokens to burn = asset token withdrawn * total LP supply / asset token pool balance const totalLpSupply = await pool.totalSupply(); const lpBurnAmount = assetTokenWithdrawAmount @@ -1975,34 +2061,38 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { .div(assetReserves) .add(1); // OToken to burn = LP tokens to burn * OToken reserves / total LP supply - const oTokenBurnAmount = lpBurnAmount.mul(oTokenReserves).div(totalLpSupply); - + const oTokenBurnAmount = lpBurnAmount + .mul(oTokenReserves) + .div(totalLpSupply); + log(`OToken burn amount : ${formatUnits(oTokenBurnAmount)}`); - + return { lpBurnAmount, oTokenBurnAmount }; } - + // Calculate the OToken and asset token amounts from a withdrawAll async function calcWithdrawAllAmounts() { const { amoStrategy, gauge, pool } = fixture; - + // Get the reserves of the pool const { assetReserves, oTokenReserves } = await getPoolReserves(); - const strategyLpAmount = await gauge.balanceOf( - amoStrategy.address - ); + const strategyLpAmount = await gauge.balanceOf(amoStrategy.address); const totalLpSupply = await pool.totalSupply(); - + // asset token to withdraw = asset token pool balance * strategy LP amount / total pool LP amount const assetTokenWithdrawAmount = assetReserves .mul(strategyLpAmount) .div(totalLpSupply); // OS to burn = OS pool balance * strategy LP amount / total pool LP amount - const oTokenBurnAmount = oTokenReserves.mul(strategyLpAmount).div(totalLpSupply); - - log(`asset token withdraw amount : ${formatUnits(assetTokenWithdrawAmount)}`); + const oTokenBurnAmount = oTokenReserves + .mul(strategyLpAmount) + .div(totalLpSupply); + + log( + `asset token withdraw amount : ${formatUnits(assetTokenWithdrawAmount)}` + ); log(`oToken burn amount : ${formatUnits(oTokenBurnAmount)}`); - + return { assetTokenWithdrawAmount, oTokenBurnAmount, @@ -2013,4 +2103,4 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { module.exports = { shouldBehaveLikeAlgebraAmoStrategy, -}; \ No newline at end of file +}; diff --git a/contracts/test/strategies/oeth-supernova-amo.mainnet.fork-test.js b/contracts/test/strategies/oeth-supernova-amo.mainnet.fork-test.js index a19adb2f4c..7e2461c511 100644 --- a/contracts/test/strategies/oeth-supernova-amo.mainnet.fork-test.js +++ b/contracts/test/strategies/oeth-supernova-amo.mainnet.fork-test.js @@ -1,6 +1,7 @@ const { supernovaOETHAMOFixure, createFixtureLoader } = require("../_fixture"); -const addresses = require("../../utils/addresses"); -const { shouldBehaveLikeAlgebraAmoStrategy } = require("../behaviour/algebraAmoStrategy"); +const { + shouldBehaveLikeAlgebraAmoStrategy, +} = require("../behaviour/algebraAmoStrategy"); describe("Mainnet Fork Test: OETH Supernova AMO Strategy", function () { shouldBehaveLikeAlgebraAmoStrategy(async () => { @@ -10,12 +11,12 @@ describe("Mainnet Fork Test: OETH Supernova AMO Strategy", function () { largeAssetIn: "10000", largeOTokenIn: "10000", }, - bootstrapPool:{ + bootstrapPool: { smallAssetBootstrapIn: "50", mediumAssetBootstrapIn: "200", largeAssetBootstrapIn: "500000", }, - mintValues:{ + mintValues: { extraSmall: "0.1", extraSmallPlus: "0.2", small: "1", @@ -65,7 +66,7 @@ describe("Mainnet Fork Test: OETH Supernova AMO Strategy", function () { partialWithdrawAmount: "10", smallSwapOTokensToPool: "0.03", largeSwapOTokensToPool: "50", - overshootSwapOTokensToPool: "999", + overshootSwapOTokensToPool: "350", disallowedSwapAssetsToPool: "0.00001", }, littleMoreAsset: { @@ -91,20 +92,24 @@ describe("Mainnet Fork Test: OETH Supernova AMO Strategy", function () { poolAddAssetAmount = 0, poolAddOTokenAmount = 0, } = {}) => { - const fixtureLoader = await createFixtureLoader(supernovaOETHAMOFixure, { - assetMintAmount, - depositToStrategy, - balancePool, - poolAddWethAmount: poolAddAssetAmount, - poolAddOethAmount: poolAddOTokenAmount, - }); + const fixtureLoader = await createFixtureLoader( + supernovaOETHAMOFixure, + { + assetMintAmount, + depositToStrategy, + balancePool, + poolAddWethAmount: poolAddAssetAmount, + poolAddOethAmount: poolAddOTokenAmount, + } + ); const fixture = await fixtureLoader(); const oTokenPoolIndex = - (await fixture.supernovaPool.token0()) === fixture.oeth.address ? 0 : 1; + (await fixture.supernovaPool.token0()) === fixture.oeth.address + ? 0 + : 1; return { - addresses: addresses.mainnet, assetToken: fixture.weth, oToken: fixture.oeth, rewardToken: fixture.supernovaRewardToken, @@ -119,7 +124,7 @@ describe("Mainnet Fork Test: OETH Supernova AMO Strategy", function () { vaultSigner: fixture.oethVaultSigner, vault: fixture.oethVault, harvester: fixture.oethHarvester, - skipHarvesterTest: true, + skipHarvesterTest: false, scenarioConfig, }; }, diff --git a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js index c114415631..48714c1e02 100644 --- a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js +++ b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js @@ -1,6 +1,7 @@ const { swapXAMOFixture } = require("../../_fixture-sonic"); -const addresses = require("../../../utils/addresses"); -const { shouldBehaveLikeAlgebraAmoStrategy } = require("../../behaviour/algebraAmoStrategy"); +const { + shouldBehaveLikeAlgebraAmoStrategy, +} = require("../../behaviour/algebraAmoStrategy"); const { createFixtureLoader } = require("../../_fixture"); describe("Sonic Fork Test: SwapX AMO Strategy", function () { @@ -11,12 +12,12 @@ describe("Sonic Fork Test: SwapX AMO Strategy", function () { largeAssetIn: "10000000", largeOTokenIn: "10000000", }, - bootstrapPool:{ + bootstrapPool: { smallAssetBootstrapIn: "5000", mediumAssetBootstrapIn: "20000", largeAssetBootstrapIn: "5000000", }, - mintValues:{ + mintValues: { extraSmall: "50", extraSmallPlus: "100", small: "2000", @@ -89,14 +90,14 @@ describe("Sonic Fork Test: SwapX AMO Strategy", function () { depositToStrategy = false, balancePool = false, poolAddAssetAmount = 0, - poolAddOTokenAmount = 0 + poolAddOTokenAmount = 0, } = {}) => { const fixtureLoader = await createFixtureLoader(swapXAMOFixture, { wsMintAmount: assetMintAmount, depositToStrategy, balancePool, poolAddwSAmount: poolAddAssetAmount, - poolAddOSAmount: poolAddOTokenAmount + poolAddOSAmount: poolAddOTokenAmount, }); const fixture = await fixtureLoader(); @@ -104,7 +105,6 @@ describe("Sonic Fork Test: SwapX AMO Strategy", function () { (await fixture.swapXPool.token0()) === fixture.oSonic.address ? 0 : 1; return { - addresses: addresses.sonic, assetToken: fixture.wS, // address of the asset token in the pool oToken: fixture.oSonic, // address of the oToken in the pool rewardToken: fixture.swpx, // address of the reward token @@ -124,4 +124,4 @@ describe("Sonic Fork Test: SwapX AMO Strategy", function () { }, }; }); -}); \ No newline at end of file +}); From d571d52dd13e4602f09d091b0441afa1ee4a4c5a Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Sat, 21 Feb 2026 22:52:45 +0000 Subject: [PATCH 22/44] WETH no longer requres whitelisting --- contracts/deploy/deployActions.js | 8 +------- .../{174_supernova_AMO.js => 178_supernova_AMO.js} | 2 +- 2 files changed, 2 insertions(+), 8 deletions(-) rename contracts/deploy/mainnet/{174_supernova_AMO.js => 178_supernova_AMO.js} (98%) diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index d6521150fb..077c6e1d20 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -923,16 +923,10 @@ const deployOETHSupernovaAMOStrategyPoolAndGauge = async () => { ); console.log("Pair address:", pairAddress); - console.log("Whitelisting OETH token & WETH token as connector"); + console.log("Whitelisting OETH token"); await tokenHandler .connect(sTokenHandlerWhitelister) .whitelistToken(oeth.address); - await tokenHandler - .connect(sTokenHandlerWhitelister) - .whitelistToken(addresses.mainnet.WETH); - await tokenHandler - .connect(sTokenHandlerWhitelister) - .whitelistConnector(addresses.mainnet.WETH); poolAddress = pairAddress; diff --git a/contracts/deploy/mainnet/174_supernova_AMO.js b/contracts/deploy/mainnet/178_supernova_AMO.js similarity index 98% rename from contracts/deploy/mainnet/174_supernova_AMO.js rename to contracts/deploy/mainnet/178_supernova_AMO.js index 4b54f4d76f..6918b272bc 100644 --- a/contracts/deploy/mainnet/174_supernova_AMO.js +++ b/contracts/deploy/mainnet/178_supernova_AMO.js @@ -10,7 +10,7 @@ const { module.exports = deploymentWithGovernanceProposal( { - deployName: "174_supernova_AMO", + deployName: "178_supernova_AMO", }, async ({ ethers }) => { const cOETHVaultProxy = await ethers.getContract("OETHVaultProxy"); From f8ab0abd76c172e9a34a48fd62c3a6cc6765c4ee Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Sat, 21 Feb 2026 22:56:36 +0000 Subject: [PATCH 23/44] typo --- contracts/deploy/mainnet/178_supernova_AMO.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/deploy/mainnet/178_supernova_AMO.js b/contracts/deploy/mainnet/178_supernova_AMO.js index 6918b272bc..9ca64b9cb8 100644 --- a/contracts/deploy/mainnet/178_supernova_AMO.js +++ b/contracts/deploy/mainnet/178_supernova_AMO.js @@ -19,7 +19,7 @@ module.exports = deploymentWithGovernanceProposal( cOETHVaultProxy.address ); - // TODO: delete once the pools ang gauges are created + // TODO: delete once the pools and gauges are created const { poolAddress, gaugeAddress } = await deployOETHSupernovaAMOStrategyPoolAndGauge(); From 03e81d9694d3df58b12199f343c5cb2b578cdced Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Sun, 22 Feb 2026 00:10:52 +0000 Subject: [PATCH 24/44] fix up the harvester test --- contracts/deploy/mainnet/178_supernova_AMO.js | 2 +- .../test/behaviour/algebraAmoStrategy.js | 29 ++++++++++++++----- .../oeth-supernova-amo.mainnet.fork-test.js | 5 ++-- .../sonic/swapx-amo.sonic.fork-test.js | 3 ++ 4 files changed, 29 insertions(+), 10 deletions(-) diff --git a/contracts/deploy/mainnet/178_supernova_AMO.js b/contracts/deploy/mainnet/178_supernova_AMO.js index 9ca64b9cb8..a000c711f1 100644 --- a/contracts/deploy/mainnet/178_supernova_AMO.js +++ b/contracts/deploy/mainnet/178_supernova_AMO.js @@ -56,7 +56,7 @@ module.exports = deploymentWithGovernanceProposal( { contract: cSupernovaAMOStrategy, signature: "setHarvesterAddress(address)", - args: [addresses.multichainBuybackOperator], + args: [addresses.multichainStrategist], }, ], }; diff --git a/contracts/test/behaviour/algebraAmoStrategy.js b/contracts/test/behaviour/algebraAmoStrategy.js index f14c1d1be7..e36e0a7a78 100644 --- a/contracts/test/behaviour/algebraAmoStrategy.js +++ b/contracts/test/behaviour/algebraAmoStrategy.js @@ -81,6 +81,9 @@ const defaultScenarioConfig = { insolvent: { swapOTokensToPool: "10", }, + harvest: { + collectedBy: "strategist", // "strategist" or "harvester" + }, }; const mergeScenarioConfig = (contextConfig = {}, fixtureConfig = {}) => ({ @@ -158,6 +161,11 @@ const mergeScenarioConfig = (contextConfig = {}, fixtureConfig = {}) => ({ ...(contextConfig.insolvent || {}), ...(fixtureConfig.insolvent || {}), }, + harvest: { + ...defaultScenarioConfig.harvest, + ...(contextConfig.harvest || {}), + ...(fixtureConfig.harvest || {}), + }, fixtureSetup: { ...defaultScenarioConfig.fixtureSetup, ...(contextConfig.fixtureSetup || {}), @@ -457,10 +465,6 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { await expect(tx).to.emit(amoStrategy, "Withdrawal"); }); it("Harvester can collect rewards", async function () { - if (fixture.skipHarvesterTest) { - this.skip(); - } - const { harvester, nick, amoStrategy, gauge, rewardToken, strategist } = fixture; @@ -482,9 +486,20 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { .notifyRewardAmount(rewardToken.address, rewardAmount); // Harvest the rewards - // prettier-ignore - const tx = await harvester - .connect(nick)["harvestAndTransfer(address)"](amoStrategy.address); + let tx; + if (getScenarioConfig().harvest.collectedBy === "strategist") { + tx = await amoStrategy.connect(strategist).collectRewardTokens(); + } else if (getScenarioConfig().harvest.collectedBy === "harvester") { + // prettier-ignore + tx = await harvester + .connect(nick)["harvestAndTransfer(address)"](amoStrategy.address); + } else { + throw new Error( + `Invalid collectedBy value: ${ + getScenarioConfig().harvest.collectedBy + }` + ); + } await expect(tx).to.emit(amoStrategy, "RewardTokenCollected"); diff --git a/contracts/test/strategies/oeth-supernova-amo.mainnet.fork-test.js b/contracts/test/strategies/oeth-supernova-amo.mainnet.fork-test.js index 7e2461c511..a7a6e6ca56 100644 --- a/contracts/test/strategies/oeth-supernova-amo.mainnet.fork-test.js +++ b/contracts/test/strategies/oeth-supernova-amo.mainnet.fork-test.js @@ -80,10 +80,12 @@ describe("Mainnet Fork Test: OETH Supernova AMO Strategy", function () { insolvent: { swapOTokensToPool: "0.1", }, + harvest: { + collectedBy: "strategist", + }, }; return { - skipHarvesterTest: true, scenarioConfig, loadFixture: async ({ assetMintAmount = 0, @@ -124,7 +126,6 @@ describe("Mainnet Fork Test: OETH Supernova AMO Strategy", function () { vaultSigner: fixture.oethVaultSigner, vault: fixture.oethVault, harvester: fixture.oethHarvester, - skipHarvesterTest: false, scenarioConfig, }; }, diff --git a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js index 48714c1e02..21f387cb94 100644 --- a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js +++ b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js @@ -81,6 +81,9 @@ describe("Sonic Fork Test: SwapX AMO Strategy", function () { insolvent: { swapOTokensToPool: "10", }, + harvest: { + collectedBy: "harvester", + }, }; return { From 5c96c3070a3bf06bd1842aed821569a8b0d3d42d Mon Sep 17 00:00:00 2001 From: Nick Addison Date: Tue, 24 Feb 2026 09:36:07 +1100 Subject: [PATCH 25/44] Fix spelling in Sonic fixture Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- contracts/test/_fixture-sonic.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/test/_fixture-sonic.js b/contracts/test/_fixture-sonic.js index 3cf8637b5e..1e4f8a7c2b 100644 --- a/contracts/test/_fixture-sonic.js +++ b/contracts/test/_fixture-sonic.js @@ -284,7 +284,7 @@ async function swapXAMOFixture( )} wS needs to be minted to the vault` ); log( - `Vualt has ${formatUnits(wsBalance)} wS in balance and ${formatUnits( + `Vault has ${formatUnits(wsBalance)} wS in balance and ${formatUnits( available )} wS available considering async withdrawals` ); From 53510c131d2dd97ac8fca9770b2c09a84cff01ef Mon Sep 17 00:00:00 2001 From: Nick Addison Date: Tue, 24 Feb 2026 09:38:17 +1100 Subject: [PATCH 26/44] Update Natspec of OETHSupernovaAMOProxy Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- contracts/contracts/proxies/Proxies.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/contracts/proxies/Proxies.sol b/contracts/contracts/proxies/Proxies.sol index d80251ec95..39f70cec94 100644 --- a/contracts/contracts/proxies/Proxies.sol +++ b/contracts/contracts/proxies/Proxies.sol @@ -244,7 +244,7 @@ contract OUSDMorphoV2StrategyProxy is InitializeGovernedUpgradeabilityProxy { } /** - * @notice OETHSupernovaAMOProxy delegates calls to a SonicSwapXAMOStrategy implementation + * @notice OETHSupernovaAMOProxy delegates calls to an OETHSupernovaAMOStrategy implementation */ contract OETHSupernovaAMOProxy is InitializeGovernedUpgradeabilityProxy { From faec0cf88159172837130b3d9009b28cc4c7dac3 Mon Sep 17 00:00:00 2001 From: Nick Addison Date: Tue, 24 Feb 2026 09:38:54 +1100 Subject: [PATCH 27/44] Update comment in deployActions.js Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- contracts/deploy/deployActions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index 077c6e1d20..9f449833d9 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -979,7 +979,7 @@ const deployOETHSupernovaAMOStrategyImplementation = async ( cOETHSupernovaAMOStrategyProxy.address ); - // Initialize Sonic Curve AMO Strategy implementation + // Initialize OETH Supernova AMO Strategy implementation const depositPriceRange = parseUnits("0.01", 18); // 1% or 100 basis points const initData = cOETHSupernovaAMOStrategy.interface.encodeFunctionData( "initialize(address[],uint256)", From 57e21481796de58ebd1a26618fe3b2fd9d9153d3 Mon Sep 17 00:00:00 2001 From: Nick Addison Date: Tue, 24 Feb 2026 09:39:36 +1100 Subject: [PATCH 28/44] Update comment in Sonic fixture Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- contracts/test/_fixture-sonic.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/test/_fixture-sonic.js b/contracts/test/_fixture-sonic.js index 1e4f8a7c2b..3fbb59f606 100644 --- a/contracts/test/_fixture-sonic.js +++ b/contracts/test/_fixture-sonic.js @@ -331,7 +331,7 @@ async function swapXAMOFixture( wsAmount )} wS to the strategy. Vault ${formatUnits(wsBalance)} wS balance` ); - // The strategist deposits the WETH to the AMO strategy + // The strategist deposits wS (Wrapped S) to the AMO strategy await oSonicVault .connect(strategist) .depositToStrategy(swapXAMOStrategy.address, [wS.address], [wsAmount]); From 15bc2af6b82b89527025782a6245098ff406e7bf Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Tue, 24 Feb 2026 09:46:00 +1100 Subject: [PATCH 29/44] Fix comment in Sonic fixture --- contracts/test/_fixture-sonic.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/test/_fixture-sonic.js b/contracts/test/_fixture-sonic.js index 3fbb59f606..9590bb4a94 100644 --- a/contracts/test/_fixture-sonic.js +++ b/contracts/test/_fixture-sonic.js @@ -323,7 +323,7 @@ async function swapXAMOFixture( } } - // Add ETH to the Metapool + // Add wS (Wrapped S) to a Sonic SwapX pool if (config?.depositToStrategy) { wsBalance = await wS.balanceOf(oSonicVault.address); log( From eb15b5bd84bdae6af79f30e90cf03e1e62581658 Mon Sep 17 00:00:00 2001 From: Nick Addison Date: Fri, 27 Feb 2026 05:12:27 +1100 Subject: [PATCH 30/44] Supernova AMO additions (#2811) * Removed asset and oToken from StableSwapAMMStrategy constructor Added oToken to IVault which is used by StableSwapAMMStrategy constructor Removed calculateRedeemOutput and calculateRedeemOutputs from IVault * Added contract diagrams for OETHSupernovaAMOStrategy * Fix SwapX AMO fork tests * Fix SwapX AMO fork tests * More rounding issues with Algebra fork tests --- contracts/contracts/interfaces/IVault.sol | 23 +- .../algebra/OETHSupernovaAMOStrategy.sol | 11 +- .../contracts/strategies/algebra/README.md | 19 ++ .../algebra/StableSwapAMMStrategy.sol | 30 +-- .../sonic/SonicSwapXAMOStrategy.sol | 11 +- contracts/deploy/deployActions.js | 32 +-- .../OETHSupernovaAMOStrategyHierarchy.svg | 75 +++++++ .../OETHSupernovaAMOStrategyInteractions.svg | 211 ++++++++++++++++++ .../docs/OETHSupernovaAMOStrategySquashed.svg | 124 ++++++++++ .../docs/OETHSupernovaAMOStrategyStorage.svg | 129 +++++++++++ contracts/docs/generate.sh | 6 + .../test/behaviour/algebraAmoStrategy.js | 6 +- .../sonic/swapx-amo.sonic.fork-test.js | 2 +- 13 files changed, 603 insertions(+), 76 deletions(-) create mode 100644 contracts/contracts/strategies/algebra/README.md create mode 100644 contracts/docs/OETHSupernovaAMOStrategyHierarchy.svg create mode 100644 contracts/docs/OETHSupernovaAMOStrategyInteractions.svg create mode 100644 contracts/docs/OETHSupernovaAMOStrategySquashed.svg create mode 100644 contracts/docs/OETHSupernovaAMOStrategyStorage.svg diff --git a/contracts/contracts/interfaces/IVault.sol b/contracts/contracts/interfaces/IVault.sol index c517cf82b0..a201488fed 100644 --- a/contracts/contracts/interfaces/IVault.sol +++ b/contracts/contracts/interfaces/IVault.sol @@ -117,6 +117,8 @@ interface IVault { ) external; // VaultCore.sol + + /// @notice Deprecated: use `mint(uint256 _amount)` instead. function mint( address _asset, uint256 _amount, @@ -137,17 +139,6 @@ interface IVault { function checkBalance(address _asset) external view returns (uint256); - /// @notice Deprecated: use calculateRedeemOutput - function calculateRedeemOutputs(uint256 _amount) - external - view - returns (uint256[] memory); - - function calculateRedeemOutput(uint256 _amount) - external - view - returns (uint256); - function getAssetCount() external view returns (uint256); function getAllAssets() external view returns (address[] memory); @@ -156,13 +147,20 @@ interface IVault { function getAllStrategies() external view returns (address[] memory); - /// @notice Deprecated. + function strategies(address _addr) + external + view + returns (VaultStorage.Strategy memory); + + /// @notice Deprecated: use `asset()` instead. function isSupportedAsset(address _asset) external view returns (bool); function dripper() external view returns (address); function asset() external view returns (address); + function oToken() external view returns (address); + function initialize(address) external; function addWithdrawalQueueLiquidity() external; @@ -216,6 +214,7 @@ interface IVault { function previewYield() external view returns (uint256 yield); + /// @notice Deprecated: user `asset()` instead. function weth() external view returns (address); // slither-disable-end constable-states diff --git a/contracts/contracts/strategies/algebra/OETHSupernovaAMOStrategy.sol b/contracts/contracts/strategies/algebra/OETHSupernovaAMOStrategy.sol index 1717eb1f7e..a9d2de0b61 100644 --- a/contracts/contracts/strategies/algebra/OETHSupernovaAMOStrategy.sol +++ b/contracts/contracts/strategies/algebra/OETHSupernovaAMOStrategy.sol @@ -12,14 +12,9 @@ contract OETHSupernovaAMOStrategy is StableSwapAMMStrategy { /** * @param _baseConfig The `platformAddress` is the address of the Supernova OETH/WETH pool. * The `vaultAddress` is the address of the OETH Vault. - * @param _oeth Address of the OETH token. - * @param _weth Address of the WETH token. * @param _gauge Address of the Supernova gauge for the pool. */ - constructor( - BaseStrategyConfig memory _baseConfig, - address _oeth, - address _weth, - address _gauge - ) StableSwapAMMStrategy(_baseConfig, _oeth, _weth, _gauge) {} + constructor(BaseStrategyConfig memory _baseConfig, address _gauge) + StableSwapAMMStrategy(_baseConfig, _gauge) + {} } diff --git a/contracts/contracts/strategies/algebra/README.md b/contracts/contracts/strategies/algebra/README.md new file mode 100644 index 0000000000..2d0dc5066c --- /dev/null +++ b/contracts/contracts/strategies/algebra/README.md @@ -0,0 +1,19 @@ +# Diagrams + +## OETH Supernova AMO Strategy + +### Hierarchy + +![OETH Supernova AMO Strategy Hierarchy](../../../docs/OETHSupernovaAMOStrategyHierarchy.svg) + +### Interactions + +![OETH Supernova AMO Strategy Interactions](../../../docs/OETHSupernovaAMOStrategyInteractions.svg) + +### Squashed + +![OETH Supernova AMO Strategy Squashed](../../../docs/OETHSupernovaAMOStrategySquashed.svg) + +### Storage + +![OETH Supernova AMO Strategy Storage](../../../docs/OETHSupernovaAMOStrategyStorage.svg) diff --git a/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol b/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol index 99e039b5e7..eb54d529c7 100644 --- a/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol +++ b/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol @@ -154,20 +154,19 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy { /** * @param _baseConfig The `platformAddress` is the address of the Algebra pool. * The `vaultAddress` is the address of the Origin Sonic Vault. - * @param _oToken Address of the OToken. - * @param _asset Address of the asset token. * @param _gauge Address of the Algebra gauge for the pool. */ - constructor( - BaseStrategyConfig memory _baseConfig, - address _oToken, - address _asset, - address _gauge - ) InitializableAbstractStrategy(_baseConfig) { + constructor(BaseStrategyConfig memory _baseConfig, address _gauge) + InitializableAbstractStrategy(_baseConfig) + { + // Read the oToken address from the Vault + address oTokenMem = IVault(_baseConfig.vaultAddress).oToken(); + address assetMem = IVault(_baseConfig.vaultAddress).asset(); + // Checked both tokens are to 18 decimals require( - IBasicToken(_asset).decimals() == 18 && - IBasicToken(_oToken).decimals() == 18, + IBasicToken(assetMem).decimals() == 18 && + IBasicToken(oTokenMem).decimals() == 18, "Incorrect token decimals" ); // Check the Algebra pool is a Stable AMM (sAMM) @@ -180,21 +179,22 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy { IGauge(_gauge).TOKEN() == _baseConfig.platformAddress, "Incorrect gauge" ); - oTokenPoolIndex = IPair(_baseConfig.platformAddress).token0() == _oToken + oTokenPoolIndex = IPair(_baseConfig.platformAddress).token0() == + oTokenMem ? 0 : 1; // Check the pool tokens are correct require( IPair(_baseConfig.platformAddress).token0() == - (oTokenPoolIndex == 0 ? _oToken : _asset) && + (oTokenPoolIndex == 0 ? oTokenMem : assetMem) && IPair(_baseConfig.platformAddress).token1() == - (oTokenPoolIndex == 0 ? _asset : _oToken), + (oTokenPoolIndex == 0 ? assetMem : oTokenMem), "Incorrect pool tokens" ); // Set the immutable variables - oToken = _oToken; - asset = _asset; + oToken = oTokenMem; + asset = assetMem; pool = _baseConfig.platformAddress; gauge = _gauge; diff --git a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol index f786fa20a2..5fd18c8dfe 100644 --- a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol +++ b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol @@ -12,14 +12,9 @@ contract SonicSwapXAMOStrategy is StableSwapAMMStrategy { /** * @param _baseConfig The `platformAddress` is the address of the SwapX pool. * The `vaultAddress` is the address of the Origin Sonic Vault. - * @param _os Address of the OS token. - * @param _ws Address of the Wrapped S (wS) token. * @param _gauge Address of the SwapX gauge for the pool. */ - constructor( - BaseStrategyConfig memory _baseConfig, - address _os, - address _ws, - address _gauge - ) StableSwapAMMStrategy(_baseConfig, _os, _ws, _gauge) {} + constructor(BaseStrategyConfig memory _baseConfig, address _gauge) + StableSwapAMMStrategy(_baseConfig, _gauge) + {} } diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index 9f449833d9..f5c4265598 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -813,7 +813,6 @@ const getPlumeContracts = async () => { }; const deploySonicSwapXAMOStrategyImplementation = async () => { - const cOSonicProxy = await ethers.getContract("OSonicProxy"); const cOSonicVaultProxy = await ethers.getContract("OSonicVaultProxy"); // Deploy Sonic SwapX AMO Strategy implementation @@ -821,8 +820,6 @@ const deploySonicSwapXAMOStrategyImplementation = async () => { "SonicSwapXAMOStrategy", [ [addresses.sonic.SwapXWSOS.pool, cOSonicVaultProxy.address], - cOSonicProxy.address, - addresses.sonic.wS, addresses.sonic.SwapXWSOS.gauge, ] ); @@ -869,11 +866,7 @@ const deploySonicSwapXAMOStrategyImplementationAndInitialize = async () => { const deployOETHSupernovaAMOStrategyPoolAndGauge = async () => { // Account authorized to call createPair on the factory contract const pairBootstrapper = "0x7F8f2B6D0b0AaE8e95221Ce90B5C26B128C1Cb66"; - const topkenHandlerWhitelister = "0xD09A1388F0CcE25DA97E8bBAbf5D083E25a5Fbc6"; const sPairBootstrapper = await impersonateAndFund(pairBootstrapper); - const sTokenHandlerWhitelister = await impersonateAndFund( - topkenHandlerWhitelister - ); const oeth = await ethers.getContract("OETHProxy"); @@ -881,10 +874,6 @@ const deployOETHSupernovaAMOStrategyPoolAndGauge = async () => { "function createPair(address tokenA, address tokenB, bool stable) external returns (address pair)", "event PairCreated(address token0, address token1, bool stable, address pair, uint);", ]; - const tokenHandlerABI = [ - "function whitelistToken(address _token) external", - "function whitelistConnector(address _token) external", - ]; const pairCreatedTopic = "0xc4805696c66d7cf352fc1d6bb633ad5ee82f6cb577c453024b6e0eb8306c6fc9"; @@ -897,10 +886,6 @@ const deployOETHSupernovaAMOStrategyPoolAndGauge = async () => { gaugeManagerAbi, addresses.mainnet.supernovaGaugeManager ); - const tokenHandler = await ethers.getContractAt( - tokenHandlerABI, - await gaugeManager.tokenHandler() - ); const factory = await ethers.getContractAt( factoryABI, @@ -908,7 +893,7 @@ const deployOETHSupernovaAMOStrategyPoolAndGauge = async () => { ); let poolAddress; - log("Creating new OETH/WETH pair..."); + console.log("Creating new OETH/WETH pair..."); const tx = await factory .connect(sPairBootstrapper) .createPair(oeth.address, addresses.mainnet.WETH, true); @@ -923,14 +908,9 @@ const deployOETHSupernovaAMOStrategyPoolAndGauge = async () => { ); console.log("Pair address:", pairAddress); - console.log("Whitelisting OETH token"); - await tokenHandler - .connect(sTokenHandlerWhitelister) - .whitelistToken(oeth.address); - poolAddress = pairAddress; - log("Creating gauge for OETH/WETH..."); + console.log("Creating gauge for OETH/WETH..."); const gaugeCreatedTopic = ethers.utils.id( "GaugeCreated(address,address,address,address,address)" ); @@ -959,19 +939,13 @@ const deployOETHSupernovaAMOStrategyImplementation = async ( const cOETHSupernovaAMOStrategyProxy = await ethers.getContract( "OETHSupernovaAMOProxy" ); - const cOETHProxy = await ethers.getContract("OETHProxy"); const cOETHVaultProxy = await ethers.getContract("OETHVaultProxy"); // Deploy OETH Supernova AMO Strategy implementation that will serve // OETH Supernova AMO const dSupernovaAMOStrategy = await deployWithConfirmation( "OETHSupernovaAMOStrategy", - [ - [poolAddress, cOETHVaultProxy.address], - cOETHProxy.address, - addresses.mainnet.WETH, - gaugeAddress, - ] + [[poolAddress, cOETHVaultProxy.address], gaugeAddress] ); const cOETHSupernovaAMOStrategy = await ethers.getContractAt( diff --git a/contracts/docs/OETHSupernovaAMOStrategyHierarchy.svg b/contracts/docs/OETHSupernovaAMOStrategyHierarchy.svg new file mode 100644 index 0000000000..6141e001eb --- /dev/null +++ b/contracts/docs/OETHSupernovaAMOStrategyHierarchy.svg @@ -0,0 +1,75 @@ + + + + + + +UmlClassDiagram + + + +40 + +<<Abstract>> +Governable +../contracts/governance/Governable.sol + + + +392 + +OETHSupernovaAMOStrategy +../contracts/strategies/algebra/OETHSupernovaAMOStrategy.sol + + + +393 + +StableSwapAMMStrategy +../contracts/strategies/algebra/StableSwapAMMStrategy.sol + + + +392->393 + + + + + +246 + +<<Abstract>> +InitializableAbstractStrategy +../contracts/utils/InitializableAbstractStrategy.sol + + + +393->246 + + + + + +245 + +<<Abstract>> +Initializable +../contracts/utils/Initializable.sol + + + +246->40 + + + + + +246->245 + + + + + diff --git a/contracts/docs/OETHSupernovaAMOStrategyInteractions.svg b/contracts/docs/OETHSupernovaAMOStrategyInteractions.svg new file mode 100644 index 0000000000..e52f46975c --- /dev/null +++ b/contracts/docs/OETHSupernovaAMOStrategyInteractions.svg @@ -0,0 +1,211 @@ + + + + + + +UmlClassDiagram + + + +54 + +<<Interface>> +IBasicToken + + + +88 + +<<Interface>> +IVault + + + +286 + +<<Interface>> +IGauge + + + +287 + +<<Interface>> +IPair + + + +392 + +OETHSupernovaAMOStrategy + + + +393 + +StableSwapAMMStrategy + + + +392->393 + + + + + +393->54 + + +decimals + + + +393->88 + + +asset +burnForStrategy +mint +mintForStrategy +oToken +strategistAddr +totalValue + + + +393->286 + + +TOKEN +balanceOf +deposit +emergency +emergencyWithdraw +getReward +totalSupply +withdraw + + + +393->287 + + +approve +balanceOf +burn +decimals +getAmountOut +getReserves +isStable +mint +skim +swap +token0 +token1 +totalSupply + + + +246 + +<<Abstract>> +InitializableAbstractStrategy + + + +393->246 + + + + + +250 + +<<Library>> +StableMath + + + +393->250 + + +divPrecisely + + + +1366 + +<<Interface>> +IERC20 + + + +393->1366 + + +approve +balanceOf +totalSupply + + + +1790 + +<<Library>> +SafeERC20 + + + +393->1790 + + +safeTransfer + + + +1391 + +<<Library>> +SafeCast + + + +393->1391 + + +toInt256 + + + +246->88 + + +strategistAddr + + + +246->1366 + + +balanceOf + + + +246->1790 + + +safeTransfer + + + +1790->1366 + + +allowance + + + diff --git a/contracts/docs/OETHSupernovaAMOStrategySquashed.svg b/contracts/docs/OETHSupernovaAMOStrategySquashed.svg new file mode 100644 index 0000000000..dbbfff8a29 --- /dev/null +++ b/contracts/docs/OETHSupernovaAMOStrategySquashed.svg @@ -0,0 +1,124 @@ + + + + + + +UmlClassDiagram + + + +392 + +OETHSupernovaAMOStrategy +../contracts/strategies/algebra/OETHSupernovaAMOStrategy.sol + +Private: +   initialized: bool <<Initializable>> +   initializing: bool <<Initializable>> +   ______gap: uint256[50] <<Initializable>> +   governorPosition: bytes32 <<Governable>> +   pendingGovernorPosition: bytes32 <<Governable>> +   reentryStatusPosition: bytes32 <<Governable>> +   _deprecated_platformAddress: address <<InitializableAbstractStrategy>> +   _deprecated_vaultAddress: address <<InitializableAbstractStrategy>> +   _deprecated_rewardTokenAddress: address <<InitializableAbstractStrategy>> +   _deprecated_rewardLiquidationThreshold: uint256 <<InitializableAbstractStrategy>> +   _reserved: int256[98] <<InitializableAbstractStrategy>> +Internal: +   assetsMapped: address[] <<InitializableAbstractStrategy>> +Public: +   _NOT_ENTERED: uint256 <<Governable>> +   _ENTERED: uint256 <<Governable>> +   platformAddress: address <<InitializableAbstractStrategy>> +   vaultAddress: address <<InitializableAbstractStrategy>> +   assetToPToken: mapping(address=>address) <<InitializableAbstractStrategy>> +   harvesterAddress: address <<InitializableAbstractStrategy>> +   rewardTokenAddresses: address[] <<InitializableAbstractStrategy>> +   SOLVENCY_THRESHOLD: uint256 <<StableSwapAMMStrategy>> +   PRECISION: uint256 <<StableSwapAMMStrategy>> +   asset: address <<StableSwapAMMStrategy>> +   oToken: address <<StableSwapAMMStrategy>> +   pool: address <<StableSwapAMMStrategy>> +   gauge: address <<StableSwapAMMStrategy>> +   oTokenPoolIndex: uint256 <<StableSwapAMMStrategy>> +   maxDepeg: uint256 <<StableSwapAMMStrategy>> + +Internal: +    _governor(): (governorOut: address) <<Governable>> +    _pendingGovernor(): (pendingGovernor: address) <<Governable>> +    _setGovernor(newGovernor: address) <<Governable>> +    _setPendingGovernor(newGovernor: address) <<Governable>> +    _changeGovernor(_newGovernor: address) <<Governable>> +    _initialize(_rewardTokenAddresses: address[], _assets: address[], _pTokens: address[]) <<InitializableAbstractStrategy>> +    _collectRewardTokens() <<InitializableAbstractStrategy>> +    _setPTokenAddress(_asset: address, _pToken: address) <<InitializableAbstractStrategy>> +    _abstractSetPToken(_asset: address, _pToken: address) <<StableSwapAMMStrategy>> +    _deposit(_assetAmount: uint256): (oTokenDepositAmount: uint256, lpTokens: uint256) <<StableSwapAMMStrategy>> +    _calcTokensToMint(_assetAmount: uint256): (oTokenAmount: uint256) <<StableSwapAMMStrategy>> +    _calcTokensToBurn(_assetAmount: uint256): (lpTokens: uint256) <<StableSwapAMMStrategy>> +    _depositToPoolAndGauge(_assetAmount: uint256, _oTokenAmount: uint256): (lpTokens: uint256) <<StableSwapAMMStrategy>> +    _withdrawFromGaugeAndPool(_lpTokens: uint256) <<StableSwapAMMStrategy>> +    _emergencyWithdrawFromGaugeAndPool() <<StableSwapAMMStrategy>> +    _swapExactTokensForTokens(_amountIn: uint256, _tokenIn: address, _tokenOut: address) <<StableSwapAMMStrategy>> +    _lpValue(_lpTokens: uint256): (value: uint256) <<StableSwapAMMStrategy>> +    _invariant(_x: uint256, _y: uint256): (k: uint256) <<StableSwapAMMStrategy>> +    _solvencyAssert() <<StableSwapAMMStrategy>> +    _getPoolReserves(): (assetReserves: uint256, oTokenReserves: uint256) <<StableSwapAMMStrategy>> +    _approveBase() <<StableSwapAMMStrategy>> +External: +    transferGovernance(_newGovernor: address) <<onlyGovernor>> <<Governable>> +    claimGovernance() <<Governable>> +    collectRewardTokens() <<onlyHarvester, nonReentrant>> <<StableSwapAMMStrategy>> +    setRewardTokenAddresses(_rewardTokenAddresses: address[]) <<onlyGovernor>> <<InitializableAbstractStrategy>> +    getRewardTokenAddresses(): address[] <<InitializableAbstractStrategy>> +    setPTokenAddress(_asset: address, _pToken: address) <<onlyGovernor>> <<InitializableAbstractStrategy>> +    removePToken(_assetIndex: uint256) <<onlyGovernor>> <<InitializableAbstractStrategy>> +    setHarvesterAddress(_harvesterAddress: address) <<onlyGovernor>> <<InitializableAbstractStrategy>> +    safeApproveAllTokens() <<onlyGovernor, nonReentrant>> <<StableSwapAMMStrategy>> +    deposit(_asset: address, _assetAmount: uint256) <<onlyVault, nonReentrant, skimPool, nearBalancedPool>> <<StableSwapAMMStrategy>> +    depositAll() <<onlyVault, nonReentrant, skimPool, nearBalancedPool>> <<StableSwapAMMStrategy>> +    withdraw(_recipient: address, _asset: address, _assetAmount: uint256) <<onlyVault, nonReentrant, skimPool>> <<StableSwapAMMStrategy>> +    withdrawAll() <<onlyVaultOrGovernor, nonReentrant, skimPool>> <<StableSwapAMMStrategy>> +    checkBalance(_asset: address): (balance: uint256) <<StableSwapAMMStrategy>> +    initialize(_rewardTokenAddresses: address[], _maxDepeg: uint256) <<onlyGovernor, initializer>> <<StableSwapAMMStrategy>> +    swapAssetsToPool(_assetAmount: uint256) <<onlyStrategist, nonReentrant, improvePoolBalance, skimPool>> <<StableSwapAMMStrategy>> +    swapOTokensToPool(_oTokenAmount: uint256) <<onlyStrategist, nonReentrant, improvePoolBalance, skimPool>> <<StableSwapAMMStrategy>> +    setMaxDepeg(_maxDepeg: uint256) <<onlyGovernor>> <<StableSwapAMMStrategy>> +Public: +    <<event>> PendingGovernorshipTransfer(previousGovernor: address, newGovernor: address) <<Governable>> +    <<event>> GovernorshipTransferred(previousGovernor: address, newGovernor: address) <<Governable>> +    <<event>> PTokenAdded(_asset: address, _pToken: address) <<InitializableAbstractStrategy>> +    <<event>> PTokenRemoved(_asset: address, _pToken: address) <<InitializableAbstractStrategy>> +    <<event>> Deposit(_asset: address, _pToken: address, _amount: uint256) <<InitializableAbstractStrategy>> +    <<event>> Withdrawal(_asset: address, _pToken: address, _amount: uint256) <<InitializableAbstractStrategy>> +    <<event>> RewardTokenCollected(recipient: address, rewardToken: address, amount: uint256) <<InitializableAbstractStrategy>> +    <<event>> RewardTokenAddressesUpdated(_oldAddresses: address[], _newAddresses: address[]) <<InitializableAbstractStrategy>> +    <<event>> HarvesterAddressesUpdated(_oldHarvesterAddress: address, _newHarvesterAddress: address) <<InitializableAbstractStrategy>> +    <<event>> SwapOTokensToPool(oTokenMinted: uint256, assetDepositAmount: uint256, oTokenDepositAmount: uint256, lpTokens: uint256) <<StableSwapAMMStrategy>> +    <<event>> SwapAssetsToPool(assetSwapped: uint256, lpTokens: uint256, oTokenBurnt: uint256) <<StableSwapAMMStrategy>> +    <<event>> MaxDepegUpdated(maxDepeg: uint256) <<StableSwapAMMStrategy>> +    <<modifier>> initializer() <<Initializable>> +    <<modifier>> onlyGovernor() <<Governable>> +    <<modifier>> nonReentrant() <<Governable>> +    <<modifier>> onlyGovernorOrStrategist() <<InitializableAbstractStrategy>> +    <<modifier>> onlyVault() <<InitializableAbstractStrategy>> +    <<modifier>> onlyHarvester() <<InitializableAbstractStrategy>> +    <<modifier>> onlyVaultOrGovernor() <<InitializableAbstractStrategy>> +    <<modifier>> onlyVaultOrGovernorOrStrategist() <<InitializableAbstractStrategy>> +    <<modifier>> onlyStrategist() <<StableSwapAMMStrategy>> +    <<modifier>> skimPool() <<StableSwapAMMStrategy>> +    <<modifier>> nearBalancedPool() <<StableSwapAMMStrategy>> +    <<modifier>> improvePoolBalance() <<StableSwapAMMStrategy>> +    governor(): address <<Governable>> +    isGovernor(): bool <<Governable>> +    constructor(_config: BaseStrategyConfig) <<InitializableAbstractStrategy>> +    transferToken(_asset: address, _amount: uint256) <<onlyGovernor>> <<InitializableAbstractStrategy>> +    supportsAsset(_asset: address): bool <<StableSwapAMMStrategy>> +    constructor(_baseConfig: BaseStrategyConfig, _gauge: address) <<OETHSupernovaAMOStrategy>> + + + diff --git a/contracts/docs/OETHSupernovaAMOStrategyStorage.svg b/contracts/docs/OETHSupernovaAMOStrategyStorage.svg new file mode 100644 index 0000000000..605400b729 --- /dev/null +++ b/contracts/docs/OETHSupernovaAMOStrategyStorage.svg @@ -0,0 +1,129 @@ + + + + + + +StorageDiagram + + + +3 + +OETHSupernovaAMOStrategy <<Contract>> + +slot + +0 + +1-50 + +51 + +52 + +53 + +54 + +55 + +56 + +57 + +58 + +59-156 + +157 + +type: <inherited contract>.variable (bytes) + +unallocated (30) + +bool: Initializable.initializing (1) + +bool: Initializable.initialized (1) + +uint256[50]: Initializable.______gap (1600) + +unallocated (12) + +address: InitializableAbstractStrategy._deprecated_platformAddress (20) + +unallocated (12) + +address: InitializableAbstractStrategy._deprecated_vaultAddress (20) + +mapping(address=>address): InitializableAbstractStrategy.assetToPToken (32) + +address[]: InitializableAbstractStrategy.assetsMapped (32) + +unallocated (12) + +address: InitializableAbstractStrategy._deprecated_rewardTokenAddress (20) + +uint256: InitializableAbstractStrategy._deprecated_rewardLiquidationThreshold (32) + +unallocated (12) + +address: InitializableAbstractStrategy.harvesterAddress (20) + +address[]: InitializableAbstractStrategy.rewardTokenAddresses (32) + +int256[98]: InitializableAbstractStrategy._reserved (3136) + +uint256: StableSwapAMMStrategy.maxDepeg (32) + + + +1 + +address[]: assetsMapped <<Array>> +0x4a11f94e20a93c79f6ec743a1954ec4fc2c08429ae2122118bf234b2185c81b8 + +offset + +0 + +type: variable (bytes) + +unallocated (12) + +address (20) + + + +3:8->1 + + + + + +2 + +address[]: rewardTokenAddresses <<Array>> +0xa2999d817b6757290b50e8ecf3fa939673403dd35c97de392fdb343b4015ce9e + +offset + +0 + +type: variable (bytes) + +unallocated (12) + +address (20) + + + +3:13->2 + + + + + diff --git a/contracts/docs/generate.sh b/contracts/docs/generate.sh index 924a67b2db..0aa95283f0 100644 --- a/contracts/docs/generate.sh +++ b/contracts/docs/generate.sh @@ -65,6 +65,12 @@ sol2uml .. -v -hv -hf -he -hs -hl -hi -i prettier-plugin-solidity -b BaseCurveAM sol2uml .. -s -d 0 -b BaseCurveAMOStrategy -i prettier-plugin-solidity -o BaseCurveAMOStrategySquashed.svg sol2uml storage .. -c BaseCurveAMOStrategy -i prettier-plugin-solidity -o BaseCurveAMOStrategyStorage.svg --hideExpand ______gap,_reserved,__gap +# contracts/strategies/algebra +sol2uml .. -v -hv -hf -he -hs -hl -hi -i prettier-plugin-solidity -b OETHSupernovaAMOStrategy -o OETHSupernovaAMOStrategyHierarchy.svg +sol2uml .. -v -hv -hf -he -hs -hn -d 2 -i prettier-plugin-solidity -b OETHSupernovaAMOStrategy -o OETHSupernovaAMOStrategyInteractions.svg +sol2uml .. -s -d 0 -b OETHSupernovaAMOStrategy -i prettier-plugin-solidity -o OETHSupernovaAMOStrategySquashed.svg +sol2uml storage .. -c OETHSupernovaAMOStrategy -i prettier-plugin-solidity -o OETHSupernovaAMOStrategyStorage.svg --hideExpand ______gap,_reserved + # contracts/strategies/sonic sol2uml .. -v -hv -hf -he -hs -hl -hi -b SonicStakingStrategy -o SonicStakingStrategyHierarchy.svg sol2uml .. -s -d 0 -b SonicStakingStrategy -o SonicStakingStrategySquashed.svg diff --git a/contracts/test/behaviour/algebraAmoStrategy.js b/contracts/test/behaviour/algebraAmoStrategy.js index e36e0a7a78..ab2198e519 100644 --- a/contracts/test/behaviour/algebraAmoStrategy.js +++ b/contracts/test/behaviour/algebraAmoStrategy.js @@ -1243,7 +1243,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { "Strategy's check balance" ).to.withinRange( dataBefore.stratBalance, - dataBefore.stratBalance.add(1) + dataBefore.stratBalance.add(2) ); }); @@ -1265,7 +1265,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { "Strategy's check balance" ).to.withinRange( dataBefore.stratBalance, - dataBefore.stratBalance.add(1) + dataBefore.stratBalance.add(2) ); // Swap OToken into the pool and asset token out. @@ -1283,7 +1283,7 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { "Strategy's check balance" ).to.withinRange( dataBefore.stratBalance, - dataBefore.stratBalance.add(1) + dataBefore.stratBalance.add(2) ); }); }); diff --git a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js index 21f387cb94..a0a4d4e0c6 100644 --- a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js +++ b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js @@ -32,7 +32,7 @@ describe("Sonic Fork Test: SwapX AMO Strategy", function () { smallPoolShare: { bootstrapAssetSwapIn: "10000000", bigLiquidityAsset: "1000000", - oTokenBuffer: "2000000", + oTokenBuffer: "1800000", stressSwapOToken: "1005000", stressSwapAsset: "2000000", stressSwapAssetAlt: "1006000", From 540e4803967ee424322ebf85f93969ebe8b68853 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Thu, 26 Feb 2026 19:23:36 +0100 Subject: [PATCH 31/44] add live pool and gauge --- contracts/deploy/deployActions.js | 79 ++----------------- contracts/deploy/mainnet/178_supernova_AMO.js | 14 +--- .../test/behaviour/algebraAmoStrategy.js | 2 +- contracts/utils/addresses.js | 7 ++ 4 files changed, 15 insertions(+), 87 deletions(-) diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index f5c4265598..8d56d255be 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -24,7 +24,6 @@ const { replaceContractAt } = require("../utils/hardhat"); const { resolveContract } = require("../utils/resolvers"); const { impersonateAccount, - impersonateAndFund, getSigner, } = require("../utils/signers"); const { getDefenderSigner } = require("../utils/signersNoHardhat"); @@ -862,77 +861,7 @@ const deploySonicSwapXAMOStrategyImplementationAndInitialize = async () => { return cSonicSwapXAMOStrategy; }; -// TODO: once this gets deployed on the mainnet delete this function -const deployOETHSupernovaAMOStrategyPoolAndGauge = async () => { - // Account authorized to call createPair on the factory contract - const pairBootstrapper = "0x7F8f2B6D0b0AaE8e95221Ce90B5C26B128C1Cb66"; - const sPairBootstrapper = await impersonateAndFund(pairBootstrapper); - - const oeth = await ethers.getContract("OETHProxy"); - - const factoryABI = [ - "function createPair(address tokenA, address tokenB, bool stable) external returns (address pair)", - "event PairCreated(address token0, address token1, bool stable, address pair, uint);", - ]; - const pairCreatedTopic = - "0xc4805696c66d7cf352fc1d6bb633ad5ee82f6cb577c453024b6e0eb8306c6fc9"; - - const gaugeManagerAbi = [ - "function createGauge(address _pool, uint256 _gaugeType) external returns (address _gauge, address _internal_bribe, address _external_bribe)", - "function tokenHandler() external view returns (address)", - "event GaugeCreated(address gauge, address creator, address internal_bribe, address external_bribe, address pool)", - ]; - const gaugeManager = await ethers.getContractAt( - gaugeManagerAbi, - addresses.mainnet.supernovaGaugeManager - ); - - const factory = await ethers.getContractAt( - factoryABI, - addresses.mainnet.supernovaPairFactory - ); - - let poolAddress; - console.log("Creating new OETH/WETH pair..."); - const tx = await factory - .connect(sPairBootstrapper) - .createPair(oeth.address, addresses.mainnet.WETH, true); - - const receipt = await tx.wait(); - const pairCreatedEvent = receipt.events.find( - (e) => e.topics[0] === pairCreatedTopic - ); - const [, pairAddress] = ethers.utils.defaultAbiCoder.decode( - ["bool", "address", "uint256"], - pairCreatedEvent.data - ); - console.log("Pair address:", pairAddress); - - poolAddress = pairAddress; - - console.log("Creating gauge for OETH/WETH..."); - const gaugeCreatedTopic = ethers.utils.id( - "GaugeCreated(address,address,address,address,address)" - ); - const tx1 = await gaugeManager.createGauge(poolAddress, 0); - const receipt1 = await tx1.wait(); - const gaugeCreatedEvent = receipt1.events.find( - (e) => e.topics[0] === gaugeCreatedTopic - ); - console.log("gaugeCreatedEvent", gaugeCreatedEvent); - //const gaugeAddress = gaugeCreatedEvent.topics[1]; - const gaugeAddress = ethers.utils.getAddress( - `0x${gaugeCreatedEvent.topics[1].slice(-40)}` - ); - log(`Created gauge at ${gaugeAddress}`); - - return { poolAddress, gaugeAddress }; -}; - -const deployOETHSupernovaAMOStrategyImplementation = async ( - poolAddress, - gaugeAddress -) => { +const deployOETHSupernovaAMOStrategyImplementation = async () => { const { deployerAddr } = await getNamedAccounts(); const sDeployer = await ethers.provider.getSigner(deployerAddr); @@ -945,7 +874,10 @@ const deployOETHSupernovaAMOStrategyImplementation = async ( // OETH Supernova AMO const dSupernovaAMOStrategy = await deployWithConfirmation( "OETHSupernovaAMOStrategy", - [[poolAddress, cOETHVaultProxy.address], gaugeAddress] + [ + [addresses.mainnet.SupernovaOETHWETH.pool, cOETHVaultProxy.address], + addresses.mainnet.SupernovaOETHWETH.gauge, + ] ); const cOETHSupernovaAMOStrategy = await ethers.getContractAt( @@ -1316,7 +1248,6 @@ module.exports = { deploySonicSwapXAMOStrategyImplementation, deploySonicSwapXAMOStrategyImplementationAndInitialize, deployOETHSupernovaAMOStrategyImplementation, - deployOETHSupernovaAMOStrategyPoolAndGauge, deployProxyWithCreateX, deployCrossChainMasterStrategyImpl, deployCrossChainRemoteStrategyImpl, diff --git a/contracts/deploy/mainnet/178_supernova_AMO.js b/contracts/deploy/mainnet/178_supernova_AMO.js index a000c711f1..2412020f4b 100644 --- a/contracts/deploy/mainnet/178_supernova_AMO.js +++ b/contracts/deploy/mainnet/178_supernova_AMO.js @@ -4,8 +4,7 @@ const { deployWithConfirmation, } = require("../../utils/deploy"); const { - deployOETHSupernovaAMOStrategyImplementation, - deployOETHSupernovaAMOStrategyPoolAndGauge, + deployOETHSupernovaAMOStrategyImplementation } = require("../deployActions"); module.exports = deploymentWithGovernanceProposal( @@ -19,23 +18,14 @@ module.exports = deploymentWithGovernanceProposal( cOETHVaultProxy.address ); - // TODO: delete once the pools and gauges are created - const { poolAddress, gaugeAddress } = - await deployOETHSupernovaAMOStrategyPoolAndGauge(); - await deployWithConfirmation("OETHSupernovaAMOProxy"); const cOETHSupernovaAMOProxy = await ethers.getContract( "OETHSupernovaAMOProxy" ); - console.log("poolAddress", poolAddress); - console.log("gaugeAddress", gaugeAddress); // Deploy Sonic SwapX AMO Strategy implementation const cSupernovaAMOStrategy = - await deployOETHSupernovaAMOStrategyImplementation( - poolAddress, - gaugeAddress - ); + await deployOETHSupernovaAMOStrategyImplementation(); return { name: "Deploy Supernova AMO Strategy", diff --git a/contracts/test/behaviour/algebraAmoStrategy.js b/contracts/test/behaviour/algebraAmoStrategy.js index ab2198e519..e0e8035a78 100644 --- a/contracts/test/behaviour/algebraAmoStrategy.js +++ b/contracts/test/behaviour/algebraAmoStrategy.js @@ -199,7 +199,7 @@ const toUnitAmount = (value) => })); */ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { - describe("ForkTest: Algebra AMO Strategy", async function () { + describe.only("ForkTest: Algebra AMO Strategy", async function () { // Retry up to 3 times on CI this.retries(isCI ? 3 : 0); let fixture, context; diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js index f4c13f1e36..199aa2f28a 100644 --- a/contracts/utils/addresses.js +++ b/contracts/utils/addresses.js @@ -359,12 +359,19 @@ addresses.mainnet.toConsensus.consolidation = addresses.mainnet.toConsensus.withdrawals = "0x00000961Ef480Eb55e80D19ad83579A64c007002"; +// Supernova AMM addresses.mainnet.supernovaPairFactory = "0x5aef44edfc5a7edd30826c724ea12d7be15bdc30"; addresses.mainnet.supernovaGaugeManager = "0x19a410046Afc4203AEcE5fbFc7A6Ac1a4F517AE2"; addresses.mainnet.supernovaToken = "0x00Da8466B296E382E5Da2Bf20962D0cB87200c78"; +addresses.mainnet.SupernovaOETHWETH = {}; +addresses.mainnet.SupernovaOETHWETH.pool = + "0x6c4ced4DE136538D10CD805ff68cdE69a52469Fd"; +addresses.mainnet.SupernovaOETHWETH.gauge = + "0xE9eAc35efB37Bd839413c5b29A26C6B32AdAE1De"; + // Mainnet Merkl addresses.mainnet.CampaignCreator = "0x8BB4C975Ff3c250e0ceEA271728547f3802B36Fd"; From eff6da24cf919c7a70487c9700fb37bb1b880775 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Thu, 26 Feb 2026 19:39:44 +0100 Subject: [PATCH 32/44] fix fork tests --- contracts/test/_fixture.js | 4 ++-- contracts/test/behaviour/algebraAmoStrategy.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 943d46c05c..3ff2171628 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -1295,8 +1295,8 @@ async function supernovaOETHAMOFixure( await resetAllowance(weth, josh, oethVault.address); // Supernova deployment creates a fresh empty pool, seed it once for AMO tests. - if ((await supernovaPool.totalSupply()).eq(0)) { - const seedAmount = parseUnits("150"); + let seedAmount = parseUnits("150"); + if ((await supernovaPool.totalSupply()).lt(seedAmount.mul(2))) { await oethVault.connect(josh).mint(weth.address, seedAmount.mul(2), 0); await weth.connect(josh).transfer(supernovaPool.address, seedAmount); await oeth.connect(josh).transfer(supernovaPool.address, seedAmount); diff --git a/contracts/test/behaviour/algebraAmoStrategy.js b/contracts/test/behaviour/algebraAmoStrategy.js index e0e8035a78..ab2198e519 100644 --- a/contracts/test/behaviour/algebraAmoStrategy.js +++ b/contracts/test/behaviour/algebraAmoStrategy.js @@ -199,7 +199,7 @@ const toUnitAmount = (value) => })); */ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { - describe.only("ForkTest: Algebra AMO Strategy", async function () { + describe("ForkTest: Algebra AMO Strategy", async function () { // Retry up to 3 times on CI this.retries(isCI ? 3 : 0); let fixture, context; From e07d99527bbe0195776ad1f91d66e34267bfa0de Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Thu, 26 Feb 2026 19:40:47 +0100 Subject: [PATCH 33/44] prettier --- contracts/deploy/deployActions.js | 5 +---- contracts/deploy/mainnet/178_supernova_AMO.js | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js index 8d56d255be..1b77f96e88 100644 --- a/contracts/deploy/deployActions.js +++ b/contracts/deploy/deployActions.js @@ -22,10 +22,7 @@ const { } = require("../utils/deploy"); const { replaceContractAt } = require("../utils/hardhat"); const { resolveContract } = require("../utils/resolvers"); -const { - impersonateAccount, - getSigner, -} = require("../utils/signers"); +const { impersonateAccount, getSigner } = require("../utils/signers"); const { getDefenderSigner } = require("../utils/signersNoHardhat"); const { getTxOpts } = require("../utils/tx"); const createxAbi = require("../abi/createx.json"); diff --git a/contracts/deploy/mainnet/178_supernova_AMO.js b/contracts/deploy/mainnet/178_supernova_AMO.js index 2412020f4b..811e8953b8 100644 --- a/contracts/deploy/mainnet/178_supernova_AMO.js +++ b/contracts/deploy/mainnet/178_supernova_AMO.js @@ -4,7 +4,7 @@ const { deployWithConfirmation, } = require("../../utils/deploy"); const { - deployOETHSupernovaAMOStrategyImplementation + deployOETHSupernovaAMOStrategyImplementation, } = require("../deployActions"); module.exports = deploymentWithGovernanceProposal( From b28575dff42af34939661ff34437e2e775409ce7 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Thu, 26 Feb 2026 20:03:00 +0100 Subject: [PATCH 34/44] fix log --- contracts/test/_fixture.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index f3ff3846b7..7226545b92 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -1399,14 +1399,14 @@ async function supernovaOETHAMOFixure( // Add WETH to the pool directly. if (cfg.poolAddWethAmount > 0) { - log(`Adding ${config.poolAddOethAmount} WETH to the pool`); + log(`Adding ${cfg.poolAddWethAmount} WETH to the pool`); const wethAmount = parseUnits(cfg.poolAddWethAmount.toString(), 18); await weth.connect(josh).transfer(supernovaPool.address, wethAmount); } // Add OETH to the pool directly. if (cfg.poolAddOethAmount > 0) { - log(`Adding ${config.poolAddOethAmount} OETH to the pool`); + log(`Adding ${cfg.poolAddOethAmount} OETH to the pool`); const oethAmount = parseUnits(cfg.poolAddOethAmount.toString(), 18); await weth.connect(josh).approve(oethVault.address, oethAmount); await oethVault.connect(josh).mint(oethAmount); From 4a7750c1e7916e68db5d1d847e01e15184437fe7 Mon Sep 17 00:00:00 2001 From: Nicholas Addison Date: Fri, 27 Feb 2026 10:40:11 +1100 Subject: [PATCH 35/44] Bumped deploy number --- .../mainnet/{178_supernova_AMO.js => 179_supernova_AMO.js} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename contracts/deploy/mainnet/{178_supernova_AMO.js => 179_supernova_AMO.js} (94%) diff --git a/contracts/deploy/mainnet/178_supernova_AMO.js b/contracts/deploy/mainnet/179_supernova_AMO.js similarity index 94% rename from contracts/deploy/mainnet/178_supernova_AMO.js rename to contracts/deploy/mainnet/179_supernova_AMO.js index 811e8953b8..33e2811b0a 100644 --- a/contracts/deploy/mainnet/178_supernova_AMO.js +++ b/contracts/deploy/mainnet/179_supernova_AMO.js @@ -9,7 +9,7 @@ const { module.exports = deploymentWithGovernanceProposal( { - deployName: "178_supernova_AMO", + deployName: "179_supernova_AMO", }, async ({ ethers }) => { const cOETHVaultProxy = await ethers.getContract("OETHVaultProxy"); @@ -23,7 +23,7 @@ module.exports = deploymentWithGovernanceProposal( "OETHSupernovaAMOProxy" ); - // Deploy Sonic SwapX AMO Strategy implementation + // Deploy Supernova AMO Strategy implementation const cSupernovaAMOStrategy = await deployOETHSupernovaAMOStrategyImplementation(); From 1625dc65d478eb00ea882b0bb1c149c0fee8d41f Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 27 Feb 2026 01:06:19 +0100 Subject: [PATCH 36/44] correct comment --- .../contracts/strategies/algebra/StableSwapAMMStrategy.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol b/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol index eb54d529c7..f4c219fc38 100644 --- a/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol +++ b/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol @@ -153,7 +153,7 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy { /** * @param _baseConfig The `platformAddress` is the address of the Algebra pool. - * The `vaultAddress` is the address of the Origin Sonic Vault. + * The `vaultAddress` is the address of the Origin Vault. * @param _gauge Address of the Algebra gauge for the pool. */ constructor(BaseStrategyConfig memory _baseConfig, address _gauge) From 6bde7d3035a59cde541195c803297b554fdf8a7f Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 27 Feb 2026 01:44:53 +0100 Subject: [PATCH 37/44] combine vault upgrade and supernova deploy into a single governance proposal --- .../algebra/StableSwapAMMStrategy.sol | 7 +- .../deploy/mainnet/172_oeth_vault_upgrade.js | 54 ------------- contracts/deploy/mainnet/179_supernova_AMO.js | 54 ------------- .../179_vault_upgrade_supernova_AMO.js | 78 +++++++++++++++++++ 4 files changed, 83 insertions(+), 110 deletions(-) delete mode 100644 contracts/deploy/mainnet/172_oeth_vault_upgrade.js delete mode 100644 contracts/deploy/mainnet/179_supernova_AMO.js create mode 100644 contracts/deploy/mainnet/179_vault_upgrade_supernova_AMO.js diff --git a/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol b/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol index f4c219fc38..b08a1a3463 100644 --- a/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol +++ b/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol @@ -160,8 +160,11 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy { InitializableAbstractStrategy(_baseConfig) { // Read the oToken address from the Vault - address oTokenMem = IVault(_baseConfig.vaultAddress).oToken(); - address assetMem = IVault(_baseConfig.vaultAddress).asset(); + // TODO: After vault upgrade remove these hardcoded values + // address oTokenMem = IVault(_baseConfig.vaultAddress).oToken(); + // address assetMem = IVault(_baseConfig.vaultAddress).asset(); + address oTokenMem = 0x856c4Efb76C1D1AE02e20CEB03A2A6a08b0b8dC3; + address assetMem = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; // Checked both tokens are to 18 decimals require( diff --git a/contracts/deploy/mainnet/172_oeth_vault_upgrade.js b/contracts/deploy/mainnet/172_oeth_vault_upgrade.js deleted file mode 100644 index 89ddb5a97e..0000000000 --- a/contracts/deploy/mainnet/172_oeth_vault_upgrade.js +++ /dev/null @@ -1,54 +0,0 @@ -const addresses = require("../../utils/addresses"); -const { deploymentWithGovernanceProposal } = require("../../utils/deploy"); - -module.exports = deploymentWithGovernanceProposal( - { - deployName: "172_oeth_vault_upgrade", - forceDeploy: false, - //forceSkip: true, - reduceQueueTime: true, - deployerIsProposer: false, - proposalId: "", - }, - async ({ deployWithConfirmation }) => { - // Deployer Actions - // ---------------- - - // 1. Deploy new OETH Vault Core and Admin implementations - const dVaultAdmin = await deployWithConfirmation( - "OETHVault", - [addresses.mainnet.WETH], - undefined, - true - ); - - // 2. Connect to the OETH Vault as its governor via the proxy - const cVaultProxy = await ethers.getContract("OETHVaultProxy"); - const cVault = await ethers.getContractAt("IVault", cVaultProxy.address); - - // 3. Connect to the Compounding Staking Strategy Proxy to set it as default strategy - const defaultStrategy = await ethers.getContract( - "CompoundingStakingSSVStrategyProxy" - ); - - // Governance Actions - // ---------------- - return { - name: "Upgrade OETH Vault to new Core and Admin implementations", - actions: [ - // 1. Upgrade the OETH Vault proxy to the new core vault implementation - { - contract: cVaultProxy, - signature: "upgradeTo(address)", - args: [dVaultAdmin.address], - }, - // 2. Set OETH/WETH AMO as default strategy - { - contract: cVault, - signature: "setDefaultStrategy(address)", - args: [defaultStrategy.address], - }, - ], - }; - } -); diff --git a/contracts/deploy/mainnet/179_supernova_AMO.js b/contracts/deploy/mainnet/179_supernova_AMO.js deleted file mode 100644 index 33e2811b0a..0000000000 --- a/contracts/deploy/mainnet/179_supernova_AMO.js +++ /dev/null @@ -1,54 +0,0 @@ -const addresses = require("../../utils/addresses"); -const { - deploymentWithGovernanceProposal, - deployWithConfirmation, -} = require("../../utils/deploy"); -const { - deployOETHSupernovaAMOStrategyImplementation, -} = require("../deployActions"); - -module.exports = deploymentWithGovernanceProposal( - { - deployName: "179_supernova_AMO", - }, - async ({ ethers }) => { - const cOETHVaultProxy = await ethers.getContract("OETHVaultProxy"); - const cOETHVaultAdmin = await ethers.getContractAt( - "VaultAdmin", - cOETHVaultProxy.address - ); - - await deployWithConfirmation("OETHSupernovaAMOProxy"); - const cOETHSupernovaAMOProxy = await ethers.getContract( - "OETHSupernovaAMOProxy" - ); - - // Deploy Supernova AMO Strategy implementation - const cSupernovaAMOStrategy = - await deployOETHSupernovaAMOStrategyImplementation(); - - return { - name: "Deploy Supernova AMO Strategy", - actions: [ - // 1. Approve new strategy on the Vault - { - contract: cOETHVaultAdmin, - signature: "approveStrategy(address)", - args: [cOETHSupernovaAMOProxy.address], - }, - // 2. Add strategy to mint whitelist - { - contract: cOETHVaultAdmin, - signature: "addStrategyToMintWhitelist(address)", - args: [cOETHSupernovaAMOProxy.address], - }, - // 3. Set the Harvester on the Supernova AMO strategy - { - contract: cSupernovaAMOStrategy, - signature: "setHarvesterAddress(address)", - args: [addresses.multichainStrategist], - }, - ], - }; - } -); diff --git a/contracts/deploy/mainnet/179_vault_upgrade_supernova_AMO.js b/contracts/deploy/mainnet/179_vault_upgrade_supernova_AMO.js new file mode 100644 index 0000000000..921a267f51 --- /dev/null +++ b/contracts/deploy/mainnet/179_vault_upgrade_supernova_AMO.js @@ -0,0 +1,78 @@ +const addresses = require("../../utils/addresses"); +const { + deploymentWithGovernanceProposal, + deployWithConfirmation, +} = require("../../utils/deploy"); +const { + deployOETHSupernovaAMOStrategyImplementation, +} = require("../deployActions"); + +module.exports = deploymentWithGovernanceProposal( + { + deployName: "179_vault_upgrade_supernova_AMO", + }, + async ({ ethers }) => { + // 1. Deploy new OETH Vault Core and Admin implementations + const dVaultAdmin = await deployWithConfirmation( + "OETHVault", + [addresses.mainnet.WETH], + undefined, + true + ); + + // 2. Connect to the OETH Vault as its governor via the proxy + const cOETHVaultProxy = await ethers.getContract("OETHVaultProxy"); + const cVault = await ethers.getContractAt("IVault", cOETHVaultProxy.address); + + // 3. Connect to the Compounding Staking Strategy Proxy to set it as default strategy + const defaultStrategy = await ethers.getContract( + "CompoundingStakingSSVStrategyProxy" + ); + + // 4. Deploy Supernova AMO Strategy implementation + await deployWithConfirmation("OETHSupernovaAMOProxy"); + const cOETHSupernovaAMOProxy = await ethers.getContract( + "OETHSupernovaAMOProxy" + ); + + // Deploy Supernova AMO Strategy implementation + const cSupernovaAMOStrategy = + await deployOETHSupernovaAMOStrategyImplementation(); + + return { + name: "Upgrade OETH Vault to new Core and Admin implementations and deploy Supernova AMO Strategy", + actions: [ + // 1. Upgrade the OETH Vault proxy to the new core vault implementation + { + contract: cOETHVaultProxy, + signature: "upgradeTo(address)", + args: [dVaultAdmin.address], + }, + // 2. Set OETH/WETH AMO as default strategy + { + contract: cVault, + signature: "setDefaultStrategy(address)", + args: [defaultStrategy.address], + }, + // 3. Approve new strategy on the Vault + { + contract: cVault, + signature: "approveStrategy(address)", + args: [cOETHSupernovaAMOProxy.address], + }, + // 4. Add strategy to mint whitelist + { + contract: cVault, + signature: "addStrategyToMintWhitelist(address)", + args: [cOETHSupernovaAMOProxy.address], + }, + // 5. Set the Harvester on the Supernova AMO strategy + { + contract: cSupernovaAMOStrategy, + signature: "setHarvesterAddress(address)", + args: [addresses.multichainStrategist], + }, + ], + }; + } +); From 5d1a14f4677a76443c62dd5063930d69cbf17ae0 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Fri, 27 Feb 2026 14:15:35 +0100 Subject: [PATCH 38/44] fix typo --- contracts/test/_fixture.js | 6 +++--- .../test/strategies/oeth-supernova-amo.mainnet.fork-test.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js index 7226545b92..bd8b7be135 100644 --- a/contracts/test/_fixture.js +++ b/contracts/test/_fixture.js @@ -1264,7 +1264,7 @@ async function instantRebaseVaultFixture(tokenName) { return fixture; } -async function supernovaOETHAMOFixure( +async function supernovaOETHAMOFixture( config = { assetMintAmount: 0, depositToStrategy: false, @@ -1277,7 +1277,7 @@ async function supernovaOETHAMOFixure( const { oeth, oethVault, weth, josh, strategist } = fixture; if (!isFork) { - throw new Error("supernovaOETHAMOFixure is only supported on fork tests"); + throw new Error("supernovaOETHAMOFixture is only supported on fork tests"); } const cfg = { @@ -1804,5 +1804,5 @@ module.exports = { autoWithdrawalModuleFixture, crossChainFixtureUnit, crossChainFixture, - supernovaOETHAMOFixure, + supernovaOETHAMOFixture, }; diff --git a/contracts/test/strategies/oeth-supernova-amo.mainnet.fork-test.js b/contracts/test/strategies/oeth-supernova-amo.mainnet.fork-test.js index a7a6e6ca56..cee4c21b40 100644 --- a/contracts/test/strategies/oeth-supernova-amo.mainnet.fork-test.js +++ b/contracts/test/strategies/oeth-supernova-amo.mainnet.fork-test.js @@ -1,4 +1,4 @@ -const { supernovaOETHAMOFixure, createFixtureLoader } = require("../_fixture"); +const { supernovaOETHAMOFixture, createFixtureLoader } = require("../_fixture"); const { shouldBehaveLikeAlgebraAmoStrategy, } = require("../behaviour/algebraAmoStrategy"); @@ -95,7 +95,7 @@ describe("Mainnet Fork Test: OETH Supernova AMO Strategy", function () { poolAddOTokenAmount = 0, } = {}) => { const fixtureLoader = await createFixtureLoader( - supernovaOETHAMOFixure, + supernovaOETHAMOFixture, { assetMintAmount, depositToStrategy, From 4e49bd83af0e7d2c609db37837d4dce6d5a19a39 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Sat, 28 Feb 2026 08:54:18 +0100 Subject: [PATCH 39/44] prettier --- .../deploy/mainnet/179_vault_upgrade_supernova_AMO.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/contracts/deploy/mainnet/179_vault_upgrade_supernova_AMO.js b/contracts/deploy/mainnet/179_vault_upgrade_supernova_AMO.js index 921a267f51..1405fd0e74 100644 --- a/contracts/deploy/mainnet/179_vault_upgrade_supernova_AMO.js +++ b/contracts/deploy/mainnet/179_vault_upgrade_supernova_AMO.js @@ -19,10 +19,13 @@ module.exports = deploymentWithGovernanceProposal( undefined, true ); - + // 2. Connect to the OETH Vault as its governor via the proxy const cOETHVaultProxy = await ethers.getContract("OETHVaultProxy"); - const cVault = await ethers.getContractAt("IVault", cOETHVaultProxy.address); + const cVault = await ethers.getContractAt( + "IVault", + cOETHVaultProxy.address + ); // 3. Connect to the Compounding Staking Strategy Proxy to set it as default strategy const defaultStrategy = await ethers.getContract( From 00a41308f237d992138f872a6d89ca9405e0a21d Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 2 Mar 2026 12:02:51 +0100 Subject: [PATCH 40/44] fix some comments --- contracts/deploy/mainnet/179_vault_upgrade_supernova_AMO.js | 2 +- contracts/deploy/sonic/027_upgrade_swapx.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/deploy/mainnet/179_vault_upgrade_supernova_AMO.js b/contracts/deploy/mainnet/179_vault_upgrade_supernova_AMO.js index 1405fd0e74..616616fb97 100644 --- a/contracts/deploy/mainnet/179_vault_upgrade_supernova_AMO.js +++ b/contracts/deploy/mainnet/179_vault_upgrade_supernova_AMO.js @@ -51,7 +51,7 @@ module.exports = deploymentWithGovernanceProposal( signature: "upgradeTo(address)", args: [dVaultAdmin.address], }, - // 2. Set OETH/WETH AMO as default strategy + // 2. Set Compounding Staking Strategy as default strategy { contract: cVault, signature: "setDefaultStrategy(address)", diff --git a/contracts/deploy/sonic/027_upgrade_swapx.js b/contracts/deploy/sonic/027_upgrade_swapx.js index 4524f2194f..8341e8ee9f 100644 --- a/contracts/deploy/sonic/027_upgrade_swapx.js +++ b/contracts/deploy/sonic/027_upgrade_swapx.js @@ -4,7 +4,7 @@ const { } = require("../deployActions"); // This is just used to confirm that the Refactoring SwapX AMO strategy into a generalized Algebra strategy is working -// as expected +// as expected. This doesn't need to be deployed on Sonic. It should stay in to not break the behavior tests. module.exports = deployOnSonic( { deployName: "027_upgrade_swapx", From cbf43449322bf0da21fd3805f6bdb5da40f29a7f Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 2 Mar 2026 12:06:37 +0100 Subject: [PATCH 41/44] send tokens after amount out calculation --- .../contracts/strategies/algebra/StableSwapAMMStrategy.sol | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol b/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol index b08a1a3463..faf6c55108 100644 --- a/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol +++ b/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol @@ -698,12 +698,13 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy { address _tokenIn, address _tokenOut ) internal { - // Transfer in tokens to the pool - IERC20(_tokenIn).safeTransfer(pool, _amountIn); - // Calculate how much out tokens we get from the swap uint256 amountOut = IPair(pool).getAmountOut(_amountIn, _tokenIn); + // Transfer in tokens to the pool after the amountOut calculation has been mde. + // This way we don't have to worry about sending tokens to pool confusing the pool's reserves. + IERC20(_tokenIn).safeTransfer(pool, _amountIn); + // Safety check that we are dealing with the correct pool tokens require( (_tokenIn == asset && _tokenOut == oToken) || From f1fb1469b6b699c90b23aa507a9abf05296a9d09 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 2 Mar 2026 12:10:29 +0100 Subject: [PATCH 42/44] add a nicer error message when pool is completely tilted into 1 direction --- .../contracts/strategies/algebra/StableSwapAMMStrategy.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol b/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol index faf6c55108..62cdbbba9b 100644 --- a/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol +++ b/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol @@ -97,6 +97,10 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy { // Get the amount of OToken received from selling 1 asset. This is buying OToken. uint256 oTokenAmount = IPair(pool).getAmountOut(1e18, asset); + + // If the pool is degenerate, then the pool is not valid and we can't deposit. + require(oTokenAmount > 0, "Pool degenerate"); + // Convert to a OToken/asset price = asset / OToken uint256 buyPrice = 1e36 / oTokenAmount; From b846277a2579077436a925fd3ec9a3f85a1f55d0 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 2 Mar 2026 12:26:59 +0100 Subject: [PATCH 43/44] add range protection for maxDepeg --- .../strategies/algebra/StableSwapAMMStrategy.sol | 4 ++++ contracts/test/behaviour/algebraAmoStrategy.js | 16 ++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol b/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol index 62cdbbba9b..4bee5b3653 100644 --- a/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol +++ b/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol @@ -842,6 +842,10 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy { * eg 0.01e18 or 1e16 is 1% which is 100 basis points. */ function setMaxDepeg(uint256 _maxDepeg) external onlyGovernor { + require( + _maxDepeg >= 0.001e18 && _maxDepeg <= 0.1e18, + "Invalid max depeg range" + ); maxDepeg = _maxDepeg; emit MaxDepegUpdated(_maxDepeg); diff --git a/contracts/test/behaviour/algebraAmoStrategy.js b/contracts/test/behaviour/algebraAmoStrategy.js index 0dff98c0ce..65bb5756e2 100644 --- a/contracts/test/behaviour/algebraAmoStrategy.js +++ b/contracts/test/behaviour/algebraAmoStrategy.js @@ -273,6 +273,22 @@ const shouldBehaveLikeAlgebraAmoStrategy = (contextFunction) => { await expect(tx).to.be.revertedWith("Caller is not the Governor"); } }); + it("Governor should fail to set max depeg too small (1bp)", async () => { + const { timelock, amoStrategy } = fixture; + + const tx = amoStrategy + .connect(timelock) + .setMaxDepeg(parseUnits("0.0001")); + await expect(tx).to.be.revertedWith("Invalid max depeg range"); + }); + it("Governor should fail to set max depeg too large (1100bp)", async () => { + const { timelock, amoStrategy } = fixture; + + const tx = amoStrategy + .connect(timelock) + .setMaxDepeg(parseUnits("0.11")); + await expect(tx).to.be.revertedWith("Invalid max depeg range"); + }); }); describe("with asset token in the vault", () => { From cac09bd3d16a746332c63aa557cfdd2579d92e27 Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Mon, 2 Mar 2026 12:30:30 +0100 Subject: [PATCH 44/44] simplify code --- .../contracts/strategies/algebra/StableSwapAMMStrategy.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol b/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol index 4bee5b3653..9273b564eb 100644 --- a/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol +++ b/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol @@ -843,7 +843,7 @@ contract StableSwapAMMStrategy is InitializableAbstractStrategy { */ function setMaxDepeg(uint256 _maxDepeg) external onlyGovernor { require( - _maxDepeg >= 0.001e18 && _maxDepeg <= 0.1e18, + _maxDepeg >= 0.001 ether && _maxDepeg <= 0.1 ether, "Invalid max depeg range" ); maxDepeg = _maxDepeg;