diff --git a/contracts/contracts/interfaces/IVault.sol b/contracts/contracts/interfaces/IVault.sol
index 55fae1009f..67999a0982 100644
--- a/contracts/contracts/interfaces/IVault.sol
+++ b/contracts/contracts/interfaces/IVault.sol
@@ -139,11 +139,18 @@ 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 asset() external view returns (address);
+ function oToken() external view returns (address);
+
function initialize(address) external;
function addWithdrawalQueueLiquidity() external;
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/proxies/Proxies.sol b/contracts/contracts/proxies/Proxies.sol
index 2dc8e89832..39f70cec94 100644
--- a/contracts/contracts/proxies/Proxies.sol
+++ b/contracts/contracts/proxies/Proxies.sol
@@ -242,3 +242,10 @@ contract CompoundingStakingSSVStrategyProxy is
contract OUSDMorphoV2StrategyProxy is InitializeGovernedUpgradeabilityProxy {
}
+
+/**
+ * @notice OETHSupernovaAMOProxy delegates calls to an OETHSupernovaAMOStrategy implementation
+ */
+contract OETHSupernovaAMOProxy is InitializeGovernedUpgradeabilityProxy {
+
+}
diff --git a/contracts/contracts/strategies/algebra/OETHSupernovaAMOStrategy.sol b/contracts/contracts/strategies/algebra/OETHSupernovaAMOStrategy.sol
new file mode 100644
index 0000000000..a9d2de0b61
--- /dev/null
+++ b/contracts/contracts/strategies/algebra/OETHSupernovaAMOStrategy.sol
@@ -0,0 +1,20 @@
+// 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 _gauge Address of the Supernova gauge for the pool.
+ */
+ 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
+
+
+
+### Interactions
+
+
+
+### Squashed
+
+
+
+### Storage
+
+
diff --git a/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol b/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol
new file mode 100644
index 0000000000..9273b564eb
--- /dev/null
+++ b/contracts/contracts/strategies/algebra/StableSwapAMMStrategy.sol
@@ -0,0 +1,883 @@
+// 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 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.
+ /// 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 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 Algebra pool in case any extra asset or OToken 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);
+
+ // 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;
+
+ 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 = asset balance - OToken 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 Algebra pool.
+ * 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)
+ InitializableAbstractStrategy(_baseConfig)
+ {
+ // Read the oToken address from the Vault
+ // 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(
+ IBasicToken(assetMem).decimals() == 18 &&
+ IBasicToken(oTokenMem).decimals() == 18,
+ "Incorrect token decimals"
+ );
+ // Check the Algebra 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() ==
+ oTokenMem
+ ? 0
+ : 1;
+ // Check the pool tokens are correct
+ require(
+ IPair(_baseConfig.platformAddress).token0() ==
+ (oTokenPoolIndex == 0 ? oTokenMem : assetMem) &&
+ IPair(_baseConfig.platformAddress).token1() ==
+ (oTokenPoolIndex == 0 ? assetMem : oTokenMem),
+ "Incorrect pool tokens"
+ );
+
+ // Set the immutable variables
+ oToken = oTokenMem;
+ asset = assetMem;
+ 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 Algebra strategies don't fit
+ * well within that abstraction.
+ * @param _rewardTokenAddresses Array containing SWPx token address
+ * @param _maxDepeg The max amount the OToken/asset 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 asset 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 Algebra 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 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 asset token.
+ * @param _assetAmount Amount of asset tokens to withdraw.
+ */
+ function withdraw(
+ address _recipient,
+ address _asset,
+ uint256 _assetAmount
+ ) external override onlyVault nonReentrant skimPool {
+ require(_assetAmount > 0, "Must withdraw something");
+ require(_asset == asset, "Unsupported asset");
+ // 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 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 OToken and any that was left in the strategy
+ uint256 oTokenToBurn = IERC20(oToken).balanceOf(address(this));
+ IVault(vaultAddress).burnForStrategy(oTokenToBurn);
+
+ // 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)) >= _assetAmount,
+ "Not enough asset removed"
+ );
+ IERC20(asset).safeTransfer(_recipient, _assetAmount);
+
+ // Ensure solvency of the vault
+ _solvencyAssert();
+
+ // 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 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.
+ */
+ 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 OToken in this strategy contract
+ uint256 oTokenToBurn = IERC20(oToken).balanceOf(address(this));
+ IVault(vaultAddress).burnForStrategy(oTokenToBurn);
+
+ // 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 assetBalance = IERC20(asset).balanceOf(address(this));
+ IERC20(asset).safeTransfer(vaultAddress, assetBalance);
+
+ // 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 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 _assetAmount)
+ external
+ onlyStrategist
+ nonReentrant
+ improvePoolBalance
+ skimPool
+ {
+ require(_assetAmount > 0, "Must swap something");
+
+ // 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 asset tokens back
+ uint256 lpTokens = _calcTokensToBurn(_assetAmount);
+ require(lpTokens > 0, "No LP tokens to burn");
+
+ _withdrawFromGaugeAndPool(lpTokens);
+
+ // 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 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 OToken tokens
+ emit Withdrawal(oToken, pool, oTokenToBurn);
+ // Emit event for the swap
+ emit SwapAssetsToPool(_assetAmount, lpTokens, oTokenToBurn);
+ }
+
+ /**
+ * @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 _oTokenAmount)
+ external
+ onlyStrategist
+ nonReentrant
+ improvePoolBalance
+ skimPool
+ {
+ require(_oTokenAmount > 0, "Must swap something");
+
+ // 1. Mint OToken so it can be swapped into the pool
+
+ // 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 OToken tokens to this strategy
+ IVault(vaultAddress).mintForStrategy(oTokenToMint);
+
+ // 2. Swap OToken for asset against the pool
+ _swapExactTokensForTokens(_oTokenAmount, oToken, asset);
+
+ // The asset is from the swap and any asset that was sitting in the strategy
+ 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
+ );
+
+ // Ensure solvency of the vault
+ _solvencyAssert();
+
+ // Emit event for the minted OToken tokens
+ emit Deposit(oToken, pool, oTokenToMint + oTokenDepositAmount);
+ // Emit event for the swap
+ emit SwapOTokensToPool(
+ oTokenToMint,
+ assetDepositAmount,
+ oTokenDepositAmount,
+ lpTokens
+ );
+ }
+
+ /***************************************
+ Assets and Rewards
+ ****************************************/
+
+ /**
+ * @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 asset token
+ * @return balance Total value in asset.
+ */
+ function checkBalance(address _asset)
+ external
+ view
+ override
+ returns (uint256 balance)
+ {
+ require(_asset == asset, "Unsupported asset");
+
+ // 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 asset and OToken tokens in the Algebra 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 Algebra Pool and Gauge Functions
+ ****************************************/
+
+ /**
+ * @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 _assetAmount)
+ internal
+ view
+ returns (uint256 oTokenAmount)
+ {
+ (uint256 assetReserves, uint256 oTokenReserves) = _getPoolReserves();
+ require(assetReserves > 0, "Empty pool");
+
+ // 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 asset tokens back
+ * from the pool.
+ * @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 _assetAmount)
+ internal
+ view
+ returns (uint256 lpTokens)
+ {
+ /* 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 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
+ * 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 assetReserves, ) = _getPoolReserves();
+ require(assetReserves > 0, "Empty pool");
+
+ lpTokens = (_assetAmount * IPair(pool).totalSupply()) / assetReserves;
+ lpTokens += 1; // Add 1 to ensure we get enough LP tokens with rounding
+ }
+
+ /**
+ * @dev Deposit asset and OToken liquidity to the Algebra pool
+ * and stake the pool's LP token in the gauge.
+ * @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 _assetAmount, uint256 _oTokenAmount)
+ internal
+ returns (uint256 lpTokens)
+ {
+ // 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));
+
+ // Deposit the pool's LP tokens into the gauge
+ IGauge(gauge).deposit(lpTokens);
+ }
+
+ /**
+ * @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(
+ 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 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 asset and OToken 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 asset and OToken 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 {
+ // 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) ||
+ (_tokenIn == oToken && _tokenOut == asset),
+ "Unsupported swap"
+ );
+
+ uint256 amount0;
+ uint256 amount1;
+
+ // Work out the correct order of the amounts for the pool
+ 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));
+
+ // The slippage protection against the amount out is indirectly done
+ // via the improvePoolBalance
+ }
+
+ /// @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 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 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(assetReserves, oTokenReserves);
+
+ // 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 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 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
+ 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 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 {
+ require(
+ _maxDepeg >= 0.001 ether && _maxDepeg <= 0.1 ether,
+ "Invalid max depeg range"
+ );
+ 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 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/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol
index 1f1206a032..5fd18c8dfe 100644
--- a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol
+++ b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol
@@ -6,816 +6,15 @@ 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 { 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");
- }
- }
+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.
- * @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
- ) 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
+ constructor(BaseStrategyConfig memory _baseConfig, address _gauge)
+ StableSwapAMMStrategy(_baseConfig, _gauge)
{}
-
- 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/deploy/deployActions.js b/contracts/deploy/deployActions.js
index 7de245e46c..07c5d4c619 100644
--- a/contracts/deploy/deployActions.js
+++ b/contracts/deploy/deployActions.js
@@ -809,13 +809,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");
// Deploy Sonic SwapX AMO Strategy implementation
@@ -823,11 +816,25 @@ const deploySonicSwapXAMOStrategyImplementation = async () => {
"SonicSwapXAMOStrategy",
[
[addresses.sonic.SwapXWSOS.pool, cOSonicVaultProxy.address],
- cOSonicProxy.address,
- addresses.sonic.wS,
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"
+ );
+
+ // Deploy Sonic SwapX AMO Strategy implementation
+ const dSonicSwapXAMOStrategy =
+ await deploySonicSwapXAMOStrategyImplementation();
+
const cSonicSwapXAMOStrategy = await ethers.getContractAt(
"SonicSwapXAMOStrategy",
cSonicSwapXAMOStrategyProxy.address
@@ -851,6 +858,49 @@ const deploySonicSwapXAMOStrategyImplementation = async () => {
return cSonicSwapXAMOStrategy;
};
+const deployOETHSupernovaAMOStrategyImplementation = async () => {
+ const { deployerAddr } = await getNamedAccounts();
+ const sDeployer = await ethers.provider.getSigner(deployerAddr);
+
+ const cOETHSupernovaAMOStrategyProxy = await ethers.getContract(
+ "OETHSupernovaAMOProxy"
+ );
+ const cOETHVaultProxy = await ethers.getContract("OETHVaultProxy");
+
+ // Deploy OETH Supernova AMO Strategy implementation that will serve
+ // OETH Supernova AMO
+ const dSupernovaAMOStrategy = await deployWithConfirmation(
+ "OETHSupernovaAMOStrategy",
+ [
+ [addresses.mainnet.SupernovaOETHWETH.pool, cOETHVaultProxy.address],
+ addresses.mainnet.SupernovaOETHWETH.gauge,
+ ]
+ );
+
+ const cOETHSupernovaAMOStrategy = await ethers.getContractAt(
+ "OETHSupernovaAMOStrategy",
+ cOETHSupernovaAMOStrategyProxy.address
+ );
+
+ // 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)",
+ [[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();
@@ -1208,6 +1258,8 @@ module.exports = {
deployBaseAerodromeAMOStrategyImplementation,
getPlumeContracts,
deploySonicSwapXAMOStrategyImplementation,
+ deploySonicSwapXAMOStrategyImplementationAndInitialize,
+ deployOETHSupernovaAMOStrategyImplementation,
deployProxyWithCreateX,
deployCrossChainMasterStrategyImpl,
deployCrossChainRemoteStrategyImpl,
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_vault_upgrade_supernova_AMO.js b/contracts/deploy/mainnet/179_vault_upgrade_supernova_AMO.js
new file mode 100644
index 0000000000..616616fb97
--- /dev/null
+++ b/contracts/deploy/mainnet/179_vault_upgrade_supernova_AMO.js
@@ -0,0 +1,81 @@
+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 Compounding Staking Strategy 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],
+ },
+ ],
+ };
+ }
+);
diff --git a/contracts/deploy/sonic/027_upgrade_swapx.js b/contracts/deploy/sonic/027_upgrade_swapx.js
new file mode 100644
index 0000000000..8341e8ee9f
--- /dev/null
+++ b/contracts/deploy/sonic/027_upgrade_swapx.js
@@ -0,0 +1,33 @@
+const { deployOnSonic } = require("../../utils/deploy-l2");
+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. 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",
+ forceSkip: false,
+ },
+ 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/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 @@
+
+
+
+
+
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 @@
+
+
+
+
+
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 @@
+
+
+
+
+
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 @@
+
+
+
+
+
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/_fixture-sonic.js b/contracts/test/_fixture-sonic.js
index 41ff1b2364..f58567d3ea 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,8 @@ 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,23 +272,66 @@ 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(
+ `Vault 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(mintAmount);
+
+ // 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
+ // Add wS (Wrapped S) to a Sonic SwapX pool
if (config?.depositToStrategy) {
- // The strategist deposits the WETH to the AMO strategy
+ wsBalance = await wS.balanceOf(oSonicVault.address);
+ log(
+ `Depositing ${formatUnits(
+ wsAmount
+ )} wS to the strategy. Vault ${formatUnits(wsBalance)} wS balance`
+ );
+ // The strategist deposits wS (Wrapped S) to the AMO strategy
await oSonicVault
.connect(strategist)
.depositToStrategy(swapXAMOStrategy.address, [wS.address], [wsAmount]);
diff --git a/contracts/test/_fixture.js b/contracts/test/_fixture.js
index 28aba6a6e8..bd8b7be135 100644
--- a/contracts/test/_fixture.js
+++ b/contracts/test/_fixture.js
@@ -36,6 +36,7 @@ const merklDistributorAbi = require("./abi/merklDistributor.json");
const curveXChainLiquidityGaugeAbi = require("./abi/curveXChainLiquidityGauge.json");
const curveStableSwapNGAbi = require("./abi/curveStableSwapNG.json");
const { defaultAbiCoder, parseUnits } = require("ethers/lib/utils");
+const { formatUnits } = ethers.utils;
const { impersonateAndFund } = require("../utils/signers");
const log = require("../utils/logger")("test:fixtures");
@@ -1263,6 +1264,168 @@ async function instantRebaseVaultFixture(tokenName) {
return fixture;
}
+async function supernovaOETHAMOFixture(
+ config = {
+ assetMintAmount: 0,
+ depositToStrategy: false,
+ balancePool: false,
+ poolAddWethAmount: 0,
+ poolAddOethAmount: 0,
+ }
+) {
+ const fixture = await defaultFixture();
+ const { oeth, oethVault, weth, josh, strategist } = fixture;
+
+ if (!isFork) {
+ throw new Error("supernovaOETHAMOFixture is only supported on fork tests");
+ }
+
+ const cfg = {
+ assetMintAmount: config?.assetMintAmount || 0,
+ depositToStrategy: config?.depositToStrategy || false,
+ balancePool: config?.balancePool || false,
+ poolAddWethAmount: config?.poolAddWethAmount || 0,
+ poolAddOethAmount: config?.poolAddOethAmount || 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.
+ let seedAmount = parseUnits("150");
+ if ((await supernovaPool.totalSupply()).lt(seedAmount.mul(2))) {
+ await oethVault.connect(josh).mint(seedAmount.mul(2));
+ 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(mintAmount);
+
+ 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.poolAddOethAmount += diff;
+ } else if (diff < 0) {
+ cfg.poolAddWethAmount += -diff;
+ }
+ }
+
+ // Add WETH to the pool directly.
+ if (cfg.poolAddWethAmount > 0) {
+ log(`Adding ${cfg.poolAddWethAmount} WETH to the pool`);
+ const wethAmount = parseUnits(cfg.poolAddWethAmount.toString(), 18);
+ await weth.connect(josh).transfer(supernovaPool.address, wethAmount);
+ }
+
+ // Add OETH to the pool directly.
+ if (cfg.poolAddOethAmount > 0) {
+ 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);
+ 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
// purposes of unit testing
async function crossChainFixtureUnit() {
@@ -1641,4 +1804,5 @@ module.exports = {
autoWithdrawalModuleFixture,
crossChainFixtureUnit,
crossChainFixture,
+ supernovaOETHAMOFixture,
};
diff --git a/contracts/test/behaviour/algebraAmoStrategy.js b/contracts/test/behaviour/algebraAmoStrategy.js
new file mode 100644
index 0000000000..65bb5756e2
--- /dev/null
+++ b/contracts/test/behaviour/algebraAmoStrategy.js
@@ -0,0 +1,2137 @@
+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");
+
+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 },
+ },
+ bootstrapPool: {
+ smallAssetBootstrapIn: "5000",
+ mediumAssetBootstrapIn: "20000",
+ largeAssetBootstrapIn: "5000000",
+ },
+ mintValues: {
+ extraSmall: "0.1",
+ extraSmallPlus: "0.2",
+ small: "1",
+ medium: "2",
+ },
+ 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",
+ },
+ harvest: {
+ collectedBy: "strategist", // "strategist" or "harvester"
+ },
+};
+
+const mergeScenarioConfig = (contextConfig = {}, fixtureConfig = {}) => ({
+ attackerFrontRun: {
+ ...defaultScenarioConfig.attackerFrontRun,
+ ...(contextConfig.attackerFrontRun || {}),
+ ...(fixtureConfig.attackerFrontRun || {}),
+ },
+ bootstrapPool: {
+ ...defaultScenarioConfig.bootstrapPool,
+ ...(contextConfig.bootstrapPool || {}),
+ ...(fixtureConfig.bootstrapPool || {}),
+ },
+ mintValues: {
+ ...defaultScenarioConfig.mintValues,
+ ...(contextConfig.mintValues || {}),
+ ...(fixtureConfig.mintValues || {}),
+ },
+ 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 || {}),
+ },
+ harvest: {
+ ...defaultScenarioConfig.harvest,
+ ...(contextConfig.harvest || {}),
+ ...(fixtureConfig.harvest || {}),
+ },
+ 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:
+ * - 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
+ 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
+ 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
+ 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("ForkTest: Algebra AMO Strategy", async function () {
+ // 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();
+ fixture = await context.loadFixture();
+ });
+ it("Should have constants and immutables set", async () => {
+ 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.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)
+ .populateTransaction.checkBalance(assetToken.address);
+ 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);
+
+ // 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 } =
+ 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);
+ 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");
+ }
+ });
+ 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", () => {
+ beforeEach(async () => {
+ context = await contextFunction();
+ fixture = await context.loadFixture({
+ assetMintAmount:
+ getScenarioConfig().bootstrapPool.largeAssetBootstrapIn,
+ depositToStrategy: false,
+ balancePool: true,
+ });
+ });
+ it("Vault should deposit asset token to AMO strategy", async function () {
+ await assertDeposit(toUnitAmount(getScenarioConfig().mintValues.small));
+ });
+ it("Only vault can deposit asset token to AMO strategy", async function () {
+ const {
+ amoStrategy,
+ vaultSigner,
+ strategist,
+ timelock,
+ nick,
+ assetToken,
+ } = fixture;
+
+ 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");
+ }
+ });
+ it("Only vault can deposit all asset tokens to AMO strategy", async function () {
+ const {
+ amoStrategy,
+ pool,
+ vaultSigner,
+ strategist,
+ timelock,
+ nick,
+ assetToken,
+ } = fixture;
+
+ 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,
+ depositToStrategy: true,
+ balancePool: true,
+ });
+ });
+ it("Vault should deposit asset token", async function () {
+ 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 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();
+
+ // 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)
+ );
+ });
+ 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)
+ );
+
+ 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"
+ );
+ }
+
+ // Governor can withdraw all
+ const tx = amoStrategy.connect(timelock).withdrawAll();
+ await expect(tx).to.emit(amoStrategy, "Withdrawal");
+ });
+ it("Harvester can collect rewards", async function () {
+ 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 gauge
+ .connect(distributorSigner)
+ .notifyRewardAmount(rewardToken.address, rewardAmount);
+
+ // Harvest the rewards
+ 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");
+
+ const rewardTokenBalanceAfter = await rewardToken.balanceOf(
+ strategist.address
+ );
+ log(
+ `Rewards collected ${formatUnits(
+ rewardTokenBalanceAfter.sub(rewardTokenBalanceBefore)
+ )}`
+ );
+ 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 attackerAssetTokenBalanceBefore = await assetToken.balanceOf(
+ nick.address
+ );
+ const assetTokenAmountIn = toUnitAmount(
+ getScenarioConfig().attackerFrontRun.moderateAssetIn
+ );
+
+ const dataBeforeSwap = await snapData();
+ logSnapData(
+ dataBeforeSwap,
+ `\nBefore attacker swaps ${formatUnits(
+ 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 depositAmount = toUnitAmount(
+ getScenarioConfig().rebalanceProbe.frontRun.depositAmount
+ );
+
+ const dataBeforeDeposit = await snapData();
+ logSnapData(
+ dataBeforeDeposit,
+ `\nAfter attacker tilted pool and before strategist deposits ${formatUnits(
+ depositAmount
+ )} asset token`
+ );
+
+ // Vault deposits wS to the strategy
+ await ensureVaultHasAssets(depositAmount);
+ 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
+ )} 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(oToken, oTokenAmountOut);
+
+ const dataAfterFinalSwap = await snapData();
+ logSnapData(
+ dataAfterFinalSwap,
+ `\nAfter attacker swaps ${formatUnits(
+ 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(attackerAssetTokenBalanceBefore)
+ )} asset token`
+ );
+ });
+
+ describe("When attacker front-run by adding a lot of asset token to the pool", () => {
+ let attackerAssetBalanceBefore;
+ let dataBeforeSwap;
+ let oTokenAmountOut;
+ beforeEach(async function () {
+ context = await contextFunction();
+ fixture = await context.loadFixture({
+ 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,
+ `\nBefore attacker swaps ${formatUnits(
+ 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
+ );
+ });
+ it("Strategist fails to deposit to strategy", async () => {
+ await assertFailedDeposit(
+ toUnitAmount(
+ getScenarioConfig().rebalanceProbe.frontRun.failedDepositAmount
+ ),
+ "price out of range"
+ );
+ });
+ it("Strategist fails to deposit all to strategy", async () => {
+ await assertFailedDepositAll(
+ toUnitAmount(
+ getScenarioConfig().rebalanceProbe.frontRun.failedDepositAllAmount
+ ),
+ "price out of range"
+ );
+ });
+ it("Strategist should withdraw from strategy with a profit", async () => {
+ 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`
+ );
+
+ 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(
+ oTokenAmountOut
+ )} 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,
+ "\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 function () {
+ context = await contextFunction();
+ fixture = await context.loadFixture({
+ assetMintAmount:
+ getScenarioConfig().rebalanceProbe.frontRun
+ .tiltSeedWithdrawAmount,
+ depositToStrategy: true,
+ });
+ const { nick, oToken, vault, assetToken } = fixture;
+
+ const oTokenAmountIn = toUnitAmount(
+ getScenarioConfig().attackerFrontRun.largeOTokenIn
+ );
+
+ // Mint OToken using asset token
+ await assetToken.connect(nick).approve(vault.address, oTokenAmountIn);
+ await vault.connect(nick).mint(oTokenAmountIn);
+
+ 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(
+ toUnitAmount(
+ getScenarioConfig().rebalanceProbe.frontRun.failedDepositAmount
+ ),
+ "price out of range"
+ );
+ });
+ it("Strategist fails to deposit all to strategy", async () => {
+ await assertFailedDepositAll(
+ toUnitAmount(
+ getScenarioConfig().rebalanceProbe.frontRun.failedDepositAllAmount
+ ),
+ "price out of range"
+ );
+ });
+ it("Strategist should withdraw from strategy with a profit", async () => {
+ 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`
+ );
+
+ 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("with a lot more OToken in the pool", () => {
+ beforeEach(async function () {
+ context = await contextFunction();
+ fixture = await context.loadFixture({
+ assetMintAmount:
+ getScenarioConfig().bootstrapPool.smallAssetBootstrapIn,
+ depositToStrategy: true,
+ balancePool: true,
+ poolAddOTokenAmount:
+ getScenarioConfig().poolImbalance.lotMoreOToken.addOToken,
+ });
+ });
+ it("Vault should fail to deposit asset token to AMO strategy", async function () {
+ 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(
+ toUnitAmount(
+ getScenarioConfig().rebalanceProbe.lotMoreOToken
+ .partialWithdrawAmount
+ )
+ );
+ });
+ it("Strategist should swap a little assets to the pool", async () => {
+ 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();
+ // 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(
+ 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(
+ 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(
+ toUnitAmount(
+ 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
+ )
+ );
+
+ 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,
+ depositToStrategy: true,
+ balancePool: true,
+ poolAddOTokenAmount:
+ getScenarioConfig().poolImbalance.littleMoreOToken.addOToken,
+ });
+ });
+ it("Vault should deposit asset token to AMO strategy", async function () {
+ 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(
+ toUnitAmount(
+ getScenarioConfig().rebalanceProbe.littleMoreOToken
+ .partialWithdrawAmount
+ )
+ );
+ });
+ it("Strategist should swap a little assets to the pool", async () => {
+ 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();
+ // 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(
+ toUnitAmount(
+ getScenarioConfig().rebalanceProbe.littleMoreOToken
+ .excessiveSwapAssetsToPool
+ )
+ );
+
+ 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(
+ 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 function () {
+ context = await contextFunction();
+ fixture = await context.loadFixture({
+ assetMintAmount:
+ getScenarioConfig().bootstrapPool.smallAssetBootstrapIn,
+ depositToStrategy: true,
+ balancePool: true,
+ poolAddAssetAmount:
+ getScenarioConfig().poolImbalance.lotMoreAsset.addAsset,
+ });
+ });
+ it("Vault should fail to deposit asset token to strategy", async function () {
+ 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(
+ toUnitAmount(
+ getScenarioConfig().rebalanceProbe.lotMoreAsset
+ .partialWithdrawAmount
+ )
+ );
+ });
+ it("Strategist should swap a little OToken to the pool", async () => {
+ await assertSwapOTokensToPool(
+ toUnitAmount(
+ getScenarioConfig().rebalanceProbe.lotMoreAsset
+ .smallSwapOTokensToPool
+ )
+ );
+ });
+ it("Strategist should swap a lot of OToken to the pool", async () => {
+ await assertSwapOTokensToPool(
+ toUnitAmount(
+ getScenarioConfig().rebalanceProbe.lotMoreAsset
+ .largeSwapOTokensToPool
+ )
+ );
+ });
+ 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 dataBefore = await snapData();
+ await logSnapData(dataBefore, "Before swapping OToken to the pool");
+
+ const tx = amoStrategy
+ .connect(strategist)
+ .swapOTokensToPool(
+ toUnitAmount(
+ getScenarioConfig().rebalanceProbe.lotMoreAsset
+ .overshootSwapOTokensToPool
+ )
+ );
+
+ 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(
+ 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 function () {
+ context = await contextFunction();
+ fixture = await context.loadFixture({
+ assetMintAmount:
+ getScenarioConfig().bootstrapPool.mediumAssetBootstrapIn,
+ depositToStrategy: true,
+ balancePool: true,
+ poolAddAssetAmount:
+ getScenarioConfig().poolImbalance.littleMoreAsset.addAsset,
+ });
+ });
+ it("Vault should deposit asset token to AMO strategy", async function () {
+ 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(
+ toUnitAmount(
+ getScenarioConfig().rebalanceProbe.littleMoreAsset
+ .partialWithdrawAmount
+ )
+ );
+ });
+ it("Strategist should swap a little OToken to the pool", async () => {
+ await assertSwapOTokensToPool(
+ toUnitAmount(
+ getScenarioConfig().rebalanceProbe.littleMoreAsset
+ .smallSwapOTokensToPool
+ )
+ );
+ });
+ 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(
+ toUnitAmount(
+ getScenarioConfig().rebalanceProbe.littleMoreAsset
+ .overshootSwapOTokensToPool
+ )
+ );
+
+ 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(
+ toUnitAmount(
+ getScenarioConfig().rebalanceProbe.littleMoreAsset
+ .disallowedSwapAssetsToPool
+ )
+ );
+
+ await expect(tx).to.be.revertedWith("Assets balance worse");
+ });
+ });
+
+ describe("with the strategy owning a small percentage of the pool", () => {
+ let dataBefore;
+
+ beforeEach(async function () {
+ context = await contextFunction();
+ fixture = await context.loadFixture({
+ assetMintAmount:
+ getScenarioConfig().bootstrapPool.smallAssetBootstrapIn,
+ depositToStrategy: true,
+ balancePool: true,
+ });
+
+ const { nick, oToken, pool, assetToken } = fixture;
+
+ // Other users add a lot more liquidity to the pool.
+ 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,
+ toUnitAmount(getScenarioConfig().smallPoolShare.bootstrapAssetSwapIn)
+ );
+ const oTokenBalance = await oToken.balanceOf(nick.address);
+ const oTokenBufferForTests = toUnitAmount(
+ getScenarioConfig().smallPoolShare.oTokenBuffer
+ );
+ 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,
+ toUnitAmount(getScenarioConfig().smallPoolShare.stressSwapOToken)
+ );
+ 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,
+ toUnitAmount(getScenarioConfig().smallPoolShare.stressSwapAsset)
+ );
+ 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(2)
+ );
+ });
+
+ 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,
+ toUnitAmount(getScenarioConfig().smallPoolShare.stressSwapAssetAlt)
+ );
+ 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(2)
+ );
+
+ // 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"
+ );
+
+ expect(
+ await amoStrategy.checkBalance(assetToken.address),
+ "Strategy's check balance"
+ ).to.withinRange(
+ dataBefore.stratBalance,
+ dataBefore.stratBalance.add(2)
+ );
+ });
+ });
+
+ describe("with an insolvent vault", () => {
+ beforeEach(async () => {
+ context = await contextFunction();
+ fixture = await context.loadFixture({
+ assetMintAmount:
+ getScenarioConfig().bootstrapPool.largeAssetBootstrapIn,
+ depositToStrategy: false,
+ });
+
+ const { vault, vaultSigner, amoStrategy, assetToken } = fixture;
+
+ // Deposit a little to the strategy.
+ const littleAmount = toUnitAmount(
+ getScenarioConfig().mintValues.extraSmallPlus
+ );
+ 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);
+
+ 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"
+ ).to.gte(lossAmount);
+ });
+
+ it("Should fail to deposit", async () => {
+ const { vaultSigner, amoStrategy, assetToken } = fixture;
+
+ // Vault calls deposit on the strategy.
+ const depositAmount = toUnitAmount(
+ getScenarioConfig().mintValues.extraSmall
+ );
+ 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,
+ toUnitAmount(getScenarioConfig().mintValues.extraSmall)
+ );
+
+ 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(
+ toUnitAmount(getScenarioConfig().mintValues.extraSmall)
+ );
+
+ 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(
+ toUnitAmount(getScenarioConfig().insolvent.swapOTokensToPool)
+ );
+
+ 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);
+ await tokenIn.connect(nick).transfer(pool.address, amountIn);
+
+ if (tokenIn.address == assetToken.address) {
+ await pool.swap(
+ oTokenPoolIndex == 1 ? 0 : amountOut,
+ oTokenPoolIndex == 1 ? amountOut : 0,
+ nick.address,
+ "0x"
+ );
+ } else {
+ await pool.swap(
+ oTokenPoolIndex == 0 ? 0 : amountOut,
+ 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
+ // 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 } = fixture;
+
+ 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 } = fixture;
+
+ 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, 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(
+ 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(
+ 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
+ delta.reserves.assetToken(assetReserves);
+ } else {
+ expect(assetReserves, "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,
+ } = fixture;
+
+ await assetToken.connect(nick).approve(vault.address, assetDepositAmount);
+ await vault.connect(nick).mint(assetDepositAmount);
+
+ 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);
+
+ // 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);
+
+ // 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,
+ });
+
+ 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 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)
+ .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
+ );
+ }
+
+ async function assertWithdrawAll() {
+ const { amoStrategy, pool, oToken, vaultSigner, assetToken } = fixture;
+
+ const dataBefore = await snapData();
+ await logSnapData(dataBefore);
+
+ 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")
+ .withArgs(assetToken.address, pool.address, assetTokenWithdrawAmount);
+ 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),
+ },
+ 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"
+ ).to.equal(0);
+ expect(
+ await oToken.balanceOf(amoStrategy.address),
+ "Strategy's oToken balance"
+ ).to.equal(0);
+ }
+
+ async function assertWithdrawPartial(assetTokenWithdrawAmount) {
+ 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")
+ .withArgs(assetToken.address, pool.address, assetTokenWithdrawAmount);
+ await expect(tx).to.emit(amoStrategy, "Withdrawal").withNamedArgs({
+ _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),
+ reserves: {
+ assetToken: (actualAssetTokenReserve) => {
+ const expectedAssetTokenReserves =
+ dataBefore.reserves.assetToken.sub(assetTokenWithdrawAmount);
+
+ expect(actualAssetTokenReserve).to.withinRange(
+ expectedAssetTokenReserves.sub(50),
+ expectedAssetTokenReserves,
+ "asset token reserves"
+ );
+ },
+ oToken: oTokenBurnAmount.mul(-1),
+ },
+ vaultAssetBalance: assetTokenWithdrawAmount,
+ gaugeSupply: lpBurnAmount.mul(-1),
+ });
+
+ 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 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);
+ // TODO this is not accurate as the liquidity needs to be removed first
+ 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 logProfit(dataBefore);
+
+ // Check emitted event
+ await expect(tx).to.emittedEvent("SwapAssetsToPool", [
+ (actualAssetTokenAmount) => {
+ expect(actualAssetTokenAmount).to.withinRange(
+ assetAmount.sub(1),
+ assetAmount.add(1),
+ "SwapAssetsToPool event asset token amount"
+ );
+ },
+ expectedLpBurnAmount,
+ (actualOTokenBurnAmount) => {
+ // TODO this can be tightened once oTokenBurnAmount is more accurately calculated
+ expect(actualOTokenBurnAmount).to.approxEqualTolerance(
+ oTokenBurnAmount,
+ 10,
+ "SwapAssetsToPool event oTokenBurnt"
+ );
+ },
+ ]);
+
+ await assertChangedData(
+ dataBefore,
+ {
+ // stratBalance: osBurnAmount.mul(-1),
+ vaultAssetBalance: 0,
+ stratGaugeBalance: 0,
+ },
+ fixture
+ );
+
+ 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 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 logProfit(dataBefore);
+
+ await assertChangedData(
+ dataBefore,
+ {
+ // stratBalance: osBurnAmount.mul(-1),
+ vaultAssetBalance: 0,
+ stratGaugeBalance: 0,
+ },
+ fixture
+ );
+
+ 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);
+ }
+
+ // 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);
+ 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 } = 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;
+
+ return { assetReserves, oTokenReserves };
+ }
+ // 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
+ .mul(totalLpSupply)
+ .div(assetReserves)
+ .add(1);
+ // OToken to burn = LP tokens to burn * OToken reserves / total LP supply
+ 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 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)}`
+ );
+ log(`oToken burn amount : ${formatUnits(oTokenBurnAmount)}`);
+
+ return {
+ assetTokenWithdrawAmount,
+ oTokenBurnAmount,
+ };
+ }
+ });
+};
+
+module.exports = {
+ shouldBehaveLikeAlgebraAmoStrategy,
+};
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..cee4c21b40
--- /dev/null
+++ b/contracts/test/strategies/oeth-supernova-amo.mainnet.fork-test.js
@@ -0,0 +1,134 @@
+const { supernovaOETHAMOFixture, createFixtureLoader } = require("../_fixture");
+const {
+ shouldBehaveLikeAlgebraAmoStrategy,
+} = require("../behaviour/algebraAmoStrategy");
+
+describe("Mainnet Fork Test: OETH Supernova AMO Strategy", function () {
+ shouldBehaveLikeAlgebraAmoStrategy(async () => {
+ const scenarioConfig = {
+ attackerFrontRun: {
+ moderateAssetIn: "20",
+ 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 },
+ lotMoreAsset: { addAsset: 400 },
+ littleMoreAsset: { addAsset: 2 },
+ },
+ smallPoolShare: {
+ bootstrapAssetSwapIn: "100",
+ bigLiquidityAsset: "50",
+ oTokenBuffer: "100",
+ stressSwapOToken: "30",
+ stressSwapAsset: "50",
+ stressSwapAssetAlt: "30",
+ },
+ rebalanceProbe: {
+ frontRun: {
+ depositAmount: "200",
+ failedDepositAmount: "200",
+ failedDepositAllAmount: "200",
+ tiltSeedWithdrawAmount: "60",
+ assetTiltWithdrawAmount: "40",
+ oTokenTiltWithdrawAmount: "0.001",
+ },
+ lotMoreOToken: {
+ failedDepositAmount: "200",
+ partialWithdrawAmount: "40",
+ smallSwapAssetsToPool: "0.3",
+ largeSwapAssetsToPool: "30",
+ nearMaxSwapAssetsToPool: "44",
+ excessiveSwapAssetsToPool: "2000",
+ disallowedSwapOTokensToPool: "0.0001",
+ },
+ littleMoreOToken: {
+ depositAmount: "12",
+ partialWithdrawAmount: "10",
+ smallSwapAssetsToPool: "0.3",
+ excessiveSwapAssetsToPool: "50",
+ disallowedSwapOTokensToPool: "0.0001",
+ },
+ lotMoreAsset: {
+ failedDepositAmount: "60",
+ partialWithdrawAmount: "10",
+ smallSwapOTokensToPool: "0.03",
+ largeSwapOTokensToPool: "50",
+ overshootSwapOTokensToPool: "350",
+ disallowedSwapAssetsToPool: "0.00001",
+ },
+ littleMoreAsset: {
+ depositAmount: "18",
+ partialWithdrawAmount: "10",
+ smallSwapOTokensToPool: "0.8",
+ overshootSwapOTokensToPool: "110",
+ disallowedSwapAssetsToPool: "0.00001",
+ },
+ },
+ insolvent: {
+ swapOTokensToPool: "0.1",
+ },
+ harvest: {
+ collectedBy: "strategist",
+ },
+ };
+
+ return {
+ scenarioConfig,
+ loadFixture: async ({
+ assetMintAmount = 0,
+ depositToStrategy = false,
+ balancePool = false,
+ poolAddAssetAmount = 0,
+ poolAddOTokenAmount = 0,
+ } = {}) => {
+ const fixtureLoader = await createFixtureLoader(
+ supernovaOETHAMOFixture,
+ {
+ assetMintAmount,
+ depositToStrategy,
+ balancePool,
+ poolAddWethAmount: poolAddAssetAmount,
+ poolAddOethAmount: poolAddOTokenAmount,
+ }
+ );
+
+ const fixture = await fixtureLoader();
+ const oTokenPoolIndex =
+ (await fixture.supernovaPool.token0()) === fixture.oeth.address
+ ? 0
+ : 1;
+
+ return {
+ 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,
+ 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 5f4acaed0e..a0a4d4e0c6 100644
--- a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js
+++ b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js
@@ -1,1635 +1,130 @@
-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.ws()).to.equal(addresses.sonic.wS);
- expect(await swapXAMOStrategy.os()).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(osAmountIn);
-
- 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(bigAmount.mul(5));
- // 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);
-
- 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(wsDepositAmount);
-
- 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);
+const {
+ shouldBehaveLikeAlgebraAmoStrategy,
+} = require("../../behaviour/algebraAmoStrategy");
+const { createFixtureLoader } = require("../../_fixture");
- expect(actualWsReserve).to.withinRange(
- expectedWsReserves.sub(50),
- expectedWsReserves,
- "wS reserves"
- );
- },
- os: osBurnAmount.mul(-1),
+describe("Sonic Fork Test: SwapX AMO Strategy", function () {
+ shouldBehaveLikeAlgebraAmoStrategy(async () => {
+ const scenarioConfig = {
+ attackerFrontRun: {
+ moderateAssetIn: "20000",
+ largeAssetIn: "10000000",
+ largeOTokenIn: "10000000",
},
- 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"
- );
+ bootstrapPool: {
+ smallAssetBootstrapIn: "5000",
+ mediumAssetBootstrapIn: "20000",
+ largeAssetBootstrapIn: "5000000",
},
- expectedLpBurnAmount,
- (actualOsBurnAmount) => {
- // TODO this can be tightened once osBurnAmount is more accurately calculated
- expect(actualOsBurnAmount).to.approxEqualTolerance(
- osBurnAmount,
- 10,
- "SwapAssetsToPool event osBurnt"
- );
+ mintValues: {
+ extraSmall: "50",
+ extraSmallPlus: "100",
+ small: "2000",
+ medium: "5000",
},
- ]);
-
- await assertChangedData(
- dataBefore,
- {
- // stratBalance: osBurnAmount.mul(-1),
- vaultWSBalance: 0,
- stratGaugeBalance: 0,
+ poolImbalance: {
+ lotMoreOToken: { addOToken: 1000000 },
+ littleMoreOToken: { addOToken: 5000 },
+ lotMoreAsset: { addAsset: 2000000 },
+ littleMoreAsset: { addAsset: 20000 },
},
- 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({ osMinted: 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,
+ smallPoolShare: {
+ bootstrapAssetSwapIn: "10000000",
+ bigLiquidityAsset: "1000000",
+ oTokenBuffer: "1800000",
+ stressSwapOToken: "1005000",
+ stressSwapAsset: "2000000",
+ stressSwapAssetAlt: "1006000",
},
- 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)}`);
+ rebalanceProbe: {
+ frontRun: {
+ depositAmount: "200000",
+ failedDepositAmount: "5000",
+ failedDepositAllAmount: "5000",
+ tiltSeedWithdrawAmount: "6000",
+ 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",
+ },
+ harvest: {
+ collectedBy: "harvester",
+ },
+ };
return {
- wsWithdrawAmount,
- osBurnAmount,
+ scenarioConfig,
+ loadFixture: async ({
+ assetMintAmount = 0,
+ depositToStrategy = false,
+ balancePool = false,
+ poolAddAssetAmount = 0,
+ poolAddOTokenAmount = 0,
+ } = {}) => {
+ 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 {
+ 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
+ scenarioConfig,
+ };
+ },
};
- }
+ });
});
diff --git a/contracts/utils/addresses.js b/contracts/utils/addresses.js
index 79f1889f08..199aa2f28a 100644
--- a/contracts/utils/addresses.js
+++ b/contracts/utils/addresses.js
@@ -359,6 +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";