Repository: OriginProtocol/origin-dollar
Pull request #2464: Protect deposits to SwapX AMO
Author: login: naddison36
Created: 2025-03-27T04:53:39Z | Merged: 2025-03-28T00:22:03Z

## Changes

* Check the SwapX pool is not too imbalanced before depositing to the SwapX AMO strategy. This uses the `maxDepeg` config on the strategy that is be max deviation from peg the OS/wS price is allowed.


Diff:
diff --git a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol
index 21567e3f4e..547efd13bc 100644
--- a/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol
+++ b/contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol
@@ -44,6 +44,13 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy {
     /// @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,
@@ -55,6 +62,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy {
         uint256 lpTokens,
         uint256 osBurnt
     );
+    event MaxDepegUpdated(uint256 maxDepeg);
 
     /**
      * @dev Verifies that the caller is the Strategist of the Vault.
@@ -75,6 +83,29 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy {
         _;
     }
 
+    /**
+     * @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.
@@ -164,12 +195,12 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy {
      * 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)
-        external
-        onlyGovernor
-        initializer
-    {
+    function initialize(
+        address[] calldata _rewardTokenAddresses,
+        uint256 _maxDepeg
+    ) external onlyGovernor initializer {
         address[] memory pTokens = new address[](1);
         pTokens[0] = pool;
 
@@ -182,6 +213,8 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy {
             pTokens
         );
 
+        maxDepeg = _maxDepeg;
+
         _approveBase();
     }
 
@@ -196,6 +229,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy {
      * 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.
      */
@@ -205,6 +239,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy {
         onlyVault
         nonReentrant
         skimPool
+        nearBalancedPool
     {
         require(_asset == ws, "Unsupported asset");
         require(_wsAmount > 0, "Must deposit something");
@@ -224,8 +259,16 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy {
      * 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 {
+    function depositAll()
+        external
+        override
+        onlyVault
+        nonReentrant
+        skimPool
+        nearBalancedPool
+    {
         uint256 wsBalance = IERC20(ws).balanceOf(address(this));
         if (wsBalance > 0) {
             (uint256 osDepositAmount, ) = _deposit(wsBalance);
@@ -269,8 +312,6 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy {
     /**
      * @notice Withdraw wS and OS from the SwapX pool, burn the OS,
      * and transfer the wS to the recipient.
-     * @dev This tx must be wrapped by the VaultValueChecker.
-     * To minimize loses, the pool should be rebalanced before the withdraw.
      * @param _recipient Address of the Vault.
      * @param _asset Address of the Wrapped S (wS) contract.
      * @param _wsAmount Amount of Wrapped S (wS) to withdraw.
@@ -319,9 +360,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy {
      * remove all wS and OS from the SwapX pool,
      * burn all the OS tokens,
      * and transfer all the wS to the Vault contract.
-     * @dev This tx must be wrapped by the VaultValueChecker.
-     * To minimize loses, the pool should be rebalanced before the withdraw.
-     * There is no solvency check here as withdrawAll can be called to
+     * @dev There is no solvency check here as withdrawAll can be called to
      * quickly secure assets to the Vault in emergencies.
      */
     function withdrawAll()
@@ -423,7 +462,7 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy {
 
         // 1. Mint OS so it can be swapped into the pool
 
-        // There shouldn't be any OS in the strategy but just in case
+        // 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;
@@ -729,6 +768,21 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy {
         }
     }
 
+    /***************************************
+                    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
     ****************************************/
diff --git a/contracts/deploy/deployActions.js b/contracts/deploy/deployActions.js
index b4de0c1867..892f05e810 100644
--- a/contracts/deploy/deployActions.js
+++ b/contracts/deploy/deployActions.js
@@ -12,6 +12,7 @@ const {
 } = require("../test/helpers.js");
 const { deployWithConfirmation, withConfirmation } = require("../utils/deploy");
 const { metapoolLPCRVPid } = require("../utils/constants");
+const { parseUnits } = require("ethers/lib/utils.js");
 
 const log = require("../utils/logger")("deploy:core");
 
@@ -1347,9 +1348,10 @@ const deploySonicSwapXAMOStrategyImplementation = async () => {
     cSonicSwapXAMOStrategyProxy.address
   );
   // Initialize Sonic Curve AMO Strategy implementation
+  const depositPriceRange = parseUnits("0.01", 18); // 1% or 100 basis points
   const initData = cSonicSwapXAMOStrategy.interface.encodeFunctionData(
-    "initialize(address[])",
-    [[addresses.sonic.SWPx]]
+    "initialize(address[],uint256)",
+    [[addresses.sonic.SWPx], depositPriceRange]
   );
   await withConfirmation(
     // prettier-ignore
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 a7f8ec3137..e79590cb0d 100644
--- a/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js
+++ b/contracts/test/strategies/sonic/swapx-amo.sonic.fork-test.js
@@ -39,6 +39,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () {
         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;
@@ -78,6 +79,35 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () {
         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", () => {
@@ -292,12 +322,12 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () {
       );
       expect(swpxBalanceAfter).to.gt(swpxBalanceBefore);
     });
-    it("Attacker front-run deposit", async () => {
+    it("Attacker front-run deposit within range", async () => {
       const { clement, oSonic, oSonicVaultSigner, swapXAMOStrategy, wS } =
         fixture;
 
       const attackerWsBalanceBefore = await wS.balanceOf(clement.address);
-      const wsAmountIn = parseUnits("5000");
+      const wsAmountIn = parseUnits("1180");
 
       const dataBeforeSwap = await snapData();
       logSnapData(
@@ -308,7 +338,7 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () {
       );
 
       // Attacker swaps a lot of wS for OS in the pool
-      // This drops the pool's price of wS
+      // This drops the pool's wS/OS price and increases the OS/wS price
       const osAmountOut = await poolSwapTokensIn(wS, wsAmountIn);
 
       const depositAmount = parseUnits("1000");
@@ -329,13 +359,13 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () {
       const dataAfterDeposit = await snapData();
       logSnapData(
         dataAfterDeposit,
-        `\nBefore attacked swaps ${formatUnits(
+        `\nBefore attacker swaps ${formatUnits(
           osAmountOut
-        )} OS into the pool for wS`
+        )} OS back into the pool for wS`
       );
       await logProfit(dataBeforeSwap);
 
-      // Attacked swaps the OS back for wS
+      // Attacker swaps the OS back for wS
       await poolSwapTokensIn(oSonic, osAmountOut);
 
       const dataAfterFinalSwap = await snapData();
@@ -352,6 +382,182 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () {
         )} 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("5000");
+
+        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("5000");
+        // Mint OS using wS
+        await oSonicVault.connect(clement).mint(wS.address, osAmountIn, 0);
+
+        attackerBalanceBefore.os = await oSonic.balanceOf(clement.address);
+        attackerBalanceBefore.ws = await wS.balanceOf(clement.address);
+
+        dataBeforeSwap = await snapData();
+        logSnapData(
+          dataBeforeSwap,
+          `\nBefore attacker swaps ${formatUnits(
+            osAmountIn
+          )} OS into the pool for wS`
+        );
+
+        // Attacker swaps a lot of OS for wS in the pool
+        // This increases the pool's wS/OS price and decreases the OS/wS price
+        wsAmountOut = await poolSwapTokensIn(oSonic, osAmountIn);
+      });
+      it("Strategist fails to deposit to strategy", async () => {
+        await assertFailedDeposit(parseUnits("5000"), "price out of range");
+      });
+      it("Strategist fails to deposit all to strategy", async () => {
+        await assertFailedDepositAll(parseUnits("5000"), "price out of range");
+      });
+      it("Strategist should withdraw from strategy with a profit", async () => {
+        const {
+          clement,
+          oSonic,
+          oSonicVault,
+          oSonicVaultSigner,
+          swapXAMOStrategy,
+          wS,
+        } = fixture;
+        const withdrawAmount = parseUnits("2000");
+
+        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", () => {
@@ -364,8 +570,8 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () {
     beforeEach(async () => {
       fixture = await loadFixture();
     });
-    it("Vault should deposit wS to AMO strategy", async function () {
-      await assertDeposit(parseUnits("5000"));
+    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();
@@ -396,15 +602,6 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () {
       // TODO calculate how much wS should be swapped to get the pool balanced
       await assertSwapAssetsToPool(parseUnits("4400"));
     });
-    it("Strategist should fail to swap all wS owned by the strategy", async () => {
-      const { swapXAMOStrategy, strategist } = fixture;
-
-      const tx = swapXAMOStrategy
-        .connect(strategist)
-        .swapAssetsToPool(parseUnits("5500"));
-
-      await expect(tx).to.be.revertedWith("Assets overshot peg");
-    });
     it("Strategist should fail to add more wS than owned by the strategy", async () => {
       const { swapXAMOStrategy, strategist } = fixture;
 
@@ -497,8 +694,8 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () {
     beforeEach(async () => {
       fixture = await loadFixture();
     });
-    it("Vault should deposit wS to AMO strategy", async function () {
-      await assertDeposit(parseUnits("6000"));
+    it("Vault should failt 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();
@@ -802,8 +999,6 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () {
     const b = x.mul(x).div(precision).add(y.mul(y).div(precision));
     const k = a.mul(b).div(precision);
 
-    log(`Invariant: ${formatUnits(k)}`);
-
     return k;
   };
 
@@ -859,6 +1054,19 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () {
     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
@@ -873,6 +1081,8 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () {
       vaultAssets,
       poolSupply,
       reserves: { ws: wsReserves, os: osReserves },
+      buyPrice,
+      sellPrice,
       stratGaugeBalance,
       gaugeSupply,
       vaultWSBalance,
@@ -909,6 +1119,8 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () {
         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(
@@ -917,7 +1129,6 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () {
     );
     log(`gauge supply        : ${formatUnits(data.gaugeSupply)}`);
     log(`vault wS balance    : ${formatUnits(data.vaultWSBalance)}`);
-    log(`strat wS balance    : ${formatUnits(data.stratWSBalance)}`);
   };
 
   const logProfit = async (dataBefore) => {
@@ -1084,6 +1295,42 @@ describe("Sonic ForkTest: SwapX AMO Strategy", function () {
     ).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;


Reviews:
user: login: sparrowDom
body: Left a few comments inline
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-27T11:00:51Z
user: login: naddison36
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-27T11:03:35Z
user: login: naddison36
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-27T11:20:07Z
user: login: naddison36
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-27T11:48:45Z
user: login: sparrowDom
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-27T12:58:11Z
user: login: naddison36
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-27T23:56:57Z
user: login: sparrowDom
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-31T09:21:48Z

Review comments:
diff_hunk: @@ -44,6 +44,13 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy {
     /// @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 maxDepeg;
path: contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol
user: login: sparrowDom
body: this could be a public a public variable
created_at: 2025-03-27T10:21:55Z
author_association: MEMBER
diff_hunk: @@ -44,6 +44,13 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy {
     /// @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 maxDepeg;
path: contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol
user: login: sparrowDom
body: We should probably also have a governance enabled setter in case we want to override the variable?
created_at: 2025-03-27T10:22:20Z
author_association: MEMBER
diff_hunk: @@ -75,6 +82,29 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy {
         _;
     }
 
+    /**
+     * @dev Checks the pool is balanced enough to allow deposits.
+     */
+    modifier nearBalancedPool() {
path: contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol
user: login: sparrowDom
body: Could we simplify this modifier by just observing the OS priced in WS?
```
modifier nearBalancedPool() {
  uint256 wsPrice = IPair(pool).getAmountOut(1e18, os);
  require(
        wsPrice <= pegPrice + maxDepeg && wsPrice >= pegPrice - maxDepeg,
        "price out of range"
    );
    _;
}
```

It is good to keep in mind that `getAmountOut` also deducts the swap fee before returning the value, so it will be slightly inaccurate because of that: 

```
    function getAmountOut(
        uint amountIn,
        address tokenIn
    ) external view returns (uint) {
        (uint _reserve0, uint _reserve1) = (reserve0, reserve1);
        amountIn -= (amountIn * IPairFactory(factory).getFee(stable)) / 10000; // remove fee from amount received
        return _getAmountOut(amountIn, tokenIn, _reserve0, _reserve1);
    }
```
created_at: 2025-03-27T11:00:39Z
author_association: MEMBER
diff_hunk: @@ -44,6 +44,13 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy {
     /// @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 maxDepeg;
path: contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol
user: login: naddison36
body: I can't believe I missed the setter. Thanks
created_at: 2025-03-27T11:03:35Z
author_association: COLLABORATOR
diff_hunk: @@ -75,6 +82,29 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy {
         _;
     }
 
+    /**
+     * @dev Checks the pool is balanced enough to allow deposits.
+     */
+    modifier nearBalancedPool() {
path: contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol
user: login: naddison36
body: That's the implementation I started with but then I thought I should be more accurate and use the actual buy and sell prices. But the difference is small. The fee on the wSOS pool is only 1 basis point while the maxDepeg is 100 basis points.

If this was mainnet then I'd definitely go with the simpler solution as it uses less gas.

I'll think about it some more
created_at: 2025-03-27T11:20:07Z
author_association: COLLABORATOR
diff_hunk: @@ -44,6 +44,13 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy {
     /// @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 maxDepeg;
path: contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol
user: login: naddison36
body: I've pushed the setter
created_at: 2025-03-27T11:48:45Z
author_association: COLLABORATOR
diff_hunk: @@ -75,6 +82,29 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy {
         _;
     }
 
+    /**
+     * @dev Checks the pool is balanced enough to allow deposits.
+     */
+    modifier nearBalancedPool() {
path: contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol
user: login: sparrowDom
body: I we ignore the swap fee shouldn't the: `buyPrice == (1 / sellPrice)`
created_at: 2025-03-27T12:58:11Z
author_association: MEMBER
diff_hunk: @@ -75,6 +82,29 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy {
         _;
     }
 
+    /**
+     * @dev Checks the pool is balanced enough to allow deposits.
+     */
+    modifier nearBalancedPool() {
path: contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol
user: login: naddison36
body: If we ignore the fee and slippage, yes.
If we are doing large swaps to rebalance then slippage can be significant. The bigger the slippage the bigger the spread between buy and sell. 
created_at: 2025-03-27T23:56:57Z
author_association: COLLABORATOR
diff_hunk: @@ -75,6 +82,29 @@ contract SonicSwapXAMOStrategy is InitializableAbstractStrategy {
         _;
     }
 
+    /**
+     * @dev Checks the pool is balanced enough to allow deposits.
+     */
+    modifier nearBalancedPool() {
path: contracts/contracts/strategies/sonic/SonicSwapXAMOStrategy.sol
user: login: sparrowDom
body: ah yes good point
created_at: 2025-03-31T09:21:48Z
author_association: MEMBER

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-27T04:54:06Z
body: 
<!--
  0 failure: 
  1 warning:  :eyes: This PR ne...
  
  
  DangerID: danger-id-Danger;
-->


<table>
  <thead>
    <tr>
      <th width="50"></th>
      <th width="100%" data-danger-table="true">Warnings</th>
    </tr>
  </thead>
  <tbody><tr>
      <td>:warning:</td>
      <td>:eyes: This PR needs at least 2 reviewers</td>
    </tr>
  </tbody>
</table>



<p align="right">
  Generated by :no_entry_sign: <a href="https://danger.systems/js">dangerJS</a> against 9a6be984395c373ea72799da6d454312575e0071
</p>

author_association: NONE
user: login: codecov[bot]
created_at: 2025-03-27T05:02:44Z
body: ## [Codecov](https://app.codecov.io/gh/OriginProtocol/origin-dollar/pull/2464?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=OriginProtocol) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 49.57%. Comparing base [(`aa2a895`)](https://app.codecov.io/gh/OriginProtocol/origin-dollar/commit/aa2a8952074f13f120d24408d18a9ec5c07adc89?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=OriginProtocol) to head [(`9a6be98`)](https://app.codecov.io/gh/OriginProtocol/origin-dollar/commit/9a6be984395c373ea72799da6d454312575e0071?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=OriginProtocol).
> Report is 2 commits behind head on nicka/sonic-swapx-amo.

<details><summary>Additional details and impacted files</summary>


```diff
@@                    Coverage Diff                    @@
##           nicka/sonic-swapx-amo    #2464      +/-   ##
=========================================================
+ Coverage                  43.63%   49.57%   +5.94%     
=========================================================
  Files                         91       92       +1     
  Lines                       4336     4395      +59     
  Branches                    1144     1162      +18     
=========================================================
+ Hits                        1892     2179     +287     
+ Misses                      2443     2215     -228     
  Partials                       1        1              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/OriginProtocol/origin-dollar/pull/2464?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=OriginProtocol).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=OriginProtocol).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE