From 6496ef32cc39c0d582371f4e2e4278544a0acf89 Mon Sep 17 00:00:00 2001
From: Jan <mig@jankjr.dk>
Date: Thu, 27 Jun 2024 15:14:54 +0200
Subject: [PATCH 1/8] redemption contract

---
 contracts/facade/redeem/EthPlusIntoEth.sol | 150 +++++++++++++++++++++
 1 file changed, 150 insertions(+)
 create mode 100644 contracts/facade/redeem/EthPlusIntoEth.sol

diff --git a/contracts/facade/redeem/EthPlusIntoEth.sol b/contracts/facade/redeem/EthPlusIntoEth.sol
new file mode 100644
index 0000000000..9cc184b4b1
--- /dev/null
+++ b/contracts/facade/redeem/EthPlusIntoEth.sol
@@ -0,0 +1,150 @@
+// SPDX-License-Identifier: BlueOak-1.0.0
+pragma solidity 0.8.19;
+
+import { IRToken } from "../../interfaces/IRToken.sol";
+import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
+import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
+import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol";
+
+interface IRETHRouter {
+    function swapTo(
+        uint256 _uniswapPortion,
+        uint256 _balancerPortion,
+        uint256 _minTokensOut,
+        uint256 _idealTokensOut
+    ) external payable;
+
+    function swapFrom(
+        uint256 _uniswapPortion,
+        uint256 _balancerPortion,
+        uint256 _minTokensOut,
+        uint256 _idealTokensOut,
+        uint256 _tokensIn
+    ) external;
+
+    function optimiseSwapTo(uint256 _amount, uint256 _steps)
+        external
+        returns (uint256[2] memory portions, uint256 amountOut);
+
+    function optimiseSwapFrom(uint256 _amount, uint256 _steps)
+        external
+        returns (uint256[2] memory portions, uint256 amountOut);
+}
+
+interface IWSTETH is IERC20 {
+    function unwrap(uint256 _wstETHAmount) external returns (uint256);
+}
+
+interface IUniswapV2Like {
+    function swapExactTokensForETH(
+        uint256 amountIn,
+        uint256 amountOutMin,
+        // is ignored, but can be empty
+        address[] calldata path,
+        address to,
+        uint256 deadline
+    ) external returns (uint256[] memory amounts);
+
+    function getAmountsOut(uint256 amountIn, address[] calldata path)
+        external
+        view
+        returns (uint256[] memory amounts);
+}
+
+interface ICurveETHstETHStableSwap {
+    function exchange(
+        int128 i,
+        int128 j,
+        uint256 dx,
+        uint256 minDy
+    ) external payable returns (uint256);
+}
+
+interface ICurveStableSwap {
+    function exchange(
+        int128 i,
+        int128 j,
+        uint256 dx,
+        uint256 minDy,
+        address receiver
+    ) external returns (uint256);
+}
+
+contract EthPlusIntoEth is IUniswapV2Like {
+    using SafeERC20 for IERC20;
+
+    IRToken private constant ETH_PLUS = IRToken(0xE72B141DF173b999AE7c1aDcbF60Cc9833Ce56a8);
+
+    IERC20 private constant RETH = IERC20(0xae78736Cd615f374D3085123A210448E74Fc6393);
+    IRETHRouter private constant RETH_ROUTER =
+        IRETHRouter(0x16D5A408e807db8eF7c578279BEeEe6b228f1c1C);
+
+    IWSTETH private constant WSTETH = IWSTETH(0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0);
+    IERC20 private constant STETH = IERC20(0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84);
+
+    IERC4626 private constant SFRXETH = IERC4626(0xac3E018457B222d93114458476f3E3416Abbe38F);
+    IERc20 private constant FRXETH = IERc20(0x5E8422345238F34275888049021821E8E08CAa1f);
+    ICurveETHstETHStableSwap private constant CURVE_ETHSTETH_STABLE_SWAP =
+        ICurveETHstETHStableSwap(0xDC24316b9AE028F1497c275EB9192a3Ea0f67022);
+    ICurveStableSwap private constant CURVE_FRXETH_WETH =
+        ICurveStableSwap(0x9c3B46C0Ceb5B9e304FCd6D88Fc50f7DD24B31Bc);
+
+    function swapExactTokensForETH(
+        uint256 amountIn,
+        uint256 amountOutMin,
+        // is ignored, but can be empty
+        // solhint-disable-next-line unused-ignore
+        address[] calldata,
+        address to,
+        uint256 deadline
+    ) external returns (uint256[] memory amounts) {
+        require(deadline >= block.timestamp, "deadline");
+        ETH_PLUS.transferFrom(msg.sender, address(this), amountIn);
+        ETH_PLUS.redeem(ETH_PLUS.balanceOf(address(this)));
+
+        // reth -> eth
+        {
+            uint256 rethBalance = RETH.balanceOf(address(this));
+            (uint256[2] memory portions, uint256 expectedETHOut) = RETH_ROUTER.optimiseSwapFrom(
+                rethBalance,
+                2
+            );
+            RETH.approve(address(rethRouter), rethBalance);
+            RETH_ROUTER.swapFrom(
+                portions[0],
+                portions[1],
+                expectedETHOut,
+                expectedETHOut,
+                rethBalance
+            );
+        }
+
+        // wsteth -> eth
+        {
+            WSTETH.unwrap(WSTETH.balanceOf(address(this)));
+            uint256 stethBalance = STETH.balanceOf(address(this));
+            STETH.approve(address(CURVE_ETHSTETH_STABLE_SWAP), stethBalance);
+            CURVE_ETHSTETH_STABLE_SWAP.exchange(1, 0, sfrxethBalance, 0);
+        }
+
+        // sfrxeth -> eth
+        {
+            uint256 sfrxethBalance = SFRXETH.balanceOf(address(this));
+            SFRXETH.redeem(sfrxethBalance, address(this), address(this));
+            uint256 frxethBalance = FRXETH.balanceOf(address(this));
+            FRXETH.approve(address(CURVE_FRXETH_WETH), frxethBalance);
+
+            // frxeth -> weth
+            CURVE_FRXETH_WETH.exchange(1, 0, frxethBalance, 0);
+
+            // weth -> eth
+            WETH.withdraw(WETH.balanceOf(address(this)));
+        }
+
+        // solhint-disable-next-line custom-errors
+        require(this.balance >= amountOutMin, "INSUFFICIENT_OUTPUT_AMOUNT");
+        this.transfer(to, this.balance);
+    }
+
+    receive() external payable {}
+}

From 8ad7be226daff65f6d80f3950b57b151a10ee72b Mon Sep 17 00:00:00 2001
From: Jan <mig@jankjr.dk>
Date: Mon, 1 Jul 2024 09:35:42 +1000
Subject: [PATCH 2/8] Add utility function to convert ETH+ directly to ETH.

---
 contracts/facade/redeem/EthPlusIntoEth.sol    | 151 +++++++++++++-----
 .../rtoken-redemptions/EthPlusIntoEth.test.ts |  60 +++++++
 2 files changed, 171 insertions(+), 40 deletions(-)
 create mode 100644 test/rtoken-redemptions/EthPlusIntoEth.test.ts

diff --git a/contracts/facade/redeem/EthPlusIntoEth.sol b/contracts/facade/redeem/EthPlusIntoEth.sol
index 9cc184b4b1..948d2034f2 100644
--- a/contracts/facade/redeem/EthPlusIntoEth.sol
+++ b/contracts/facade/redeem/EthPlusIntoEth.sol
@@ -1,10 +1,16 @@
 // SPDX-License-Identifier: BlueOak-1.0.0
 pragma solidity 0.8.19;
 
-import { IRToken } from "../../interfaces/IRToken.sol";
 import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
 import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
 import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol";
+import { IBasketHandler } from "../../interfaces/IBasketHandler.sol";
+import { IRToken } from "../../interfaces/IRToken.sol";
+import { RoundingMode, FixLib } from "../../libraries/Fixed.sol";
+
+interface IWETH is IERC20 {
+    function withdraw(uint256 wad) external;
+}
 
 interface IRETHRouter {
     function swapTo(
@@ -33,22 +39,8 @@ interface IRETHRouter {
 
 interface IWSTETH is IERC20 {
     function unwrap(uint256 _wstETHAmount) external returns (uint256);
-}
-
-interface IUniswapV2Like {
-    function swapExactTokensForETH(
-        uint256 amountIn,
-        uint256 amountOutMin,
-        // is ignored, but can be empty
-        address[] calldata path,
-        address to,
-        uint256 deadline
-    ) external returns (uint256[] memory amounts);
 
-    function getAmountsOut(uint256 amountIn, address[] calldata path)
-        external
-        view
-        returns (uint256[] memory amounts);
+    function getStETHByWstETH(uint256 _wstETHAmount) external view returns (uint256);
 }
 
 interface ICurveETHstETHStableSwap {
@@ -58,6 +50,18 @@ interface ICurveETHstETHStableSwap {
         uint256 dx,
         uint256 minDy
     ) external payable returns (uint256);
+
+    function get_dy(
+        int128 i,
+        int128 j,
+        uint256 dx
+    ) external view returns (uint256);
+}
+
+interface IRETH is IERC20 {
+    function burn(uint256 rethAmt) external;
+
+    function getEthValue(uint256 rethAmt) external view returns (uint256);
 }
 
 interface ICurveStableSwap {
@@ -68,14 +72,42 @@ interface ICurveStableSwap {
         uint256 minDy,
         address receiver
     ) external returns (uint256);
+
+    function get_dy(
+        int128 i,
+        int128 j,
+        uint256 dx
+    ) external view returns (uint256);
 }
 
+interface IUniswapV2Like {
+    function swapExactTokensForETH(
+        uint256 amountIn,
+        uint256 amountOutMin,
+        // is ignored, can be empty
+        address[] calldata path,
+        address to,
+        uint256 deadline
+    ) external returns (uint256[] memory amounts);
+
+    function getAmountsOut(
+        uint256 amountIn,
+        // is ignored, can be empty
+        address[] calldata path
+    ) external view returns (uint256[] memory amounts);
+}
+
+/** Small utility contract to swap ETH+ for ETH by redeeming ETH+ and swapping.
+ */
 contract EthPlusIntoEth is IUniswapV2Like {
     using SafeERC20 for IERC20;
 
     IRToken private constant ETH_PLUS = IRToken(0xE72B141DF173b999AE7c1aDcbF60Cc9833Ce56a8);
 
-    IERC20 private constant RETH = IERC20(0xae78736Cd615f374D3085123A210448E74Fc6393);
+    IRETH private constant RETH = IRETH(0xae78736Cd615f374D3085123A210448E74Fc6393);
+
+    IWETH private constant WETH = IWETH(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
+
     IRETHRouter private constant RETH_ROUTER =
         IRETHRouter(0x16D5A408e807db8eF7c578279BEeEe6b228f1c1C);
 
@@ -83,48 +115,81 @@ contract EthPlusIntoEth is IUniswapV2Like {
     IERC20 private constant STETH = IERC20(0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84);
 
     IERC4626 private constant SFRXETH = IERC4626(0xac3E018457B222d93114458476f3E3416Abbe38F);
-    IERc20 private constant FRXETH = IERc20(0x5E8422345238F34275888049021821E8E08CAa1f);
+    IERC20 private constant FRXETH = IERC20(0x5E8422345238F34275888049021821E8E08CAa1f);
     ICurveETHstETHStableSwap private constant CURVE_ETHSTETH_STABLE_SWAP =
         ICurveETHstETHStableSwap(0xDC24316b9AE028F1497c275EB9192a3Ea0f67022);
     ICurveStableSwap private constant CURVE_FRXETH_WETH =
         ICurveStableSwap(0x9c3B46C0Ceb5B9e304FCd6D88Fc50f7DD24B31Bc);
 
+    function getETHPlusRedemptionQuantities(uint256 amt) external returns (uint256[] memory) {
+        IBasketHandler handler = ETH_PLUS.main().basketHandler();
+        uint256 supply = ETH_PLUS.totalSupply();
+        (, uint256[] memory quantities) = handler.quote(
+            FixLib.muluDivu(ETH_PLUS.basketsNeeded(), amt, supply, RoundingMode.CEIL),
+            RoundingMode.FLOOR
+        );
+        return quantities;
+    }
+
+    function getAmountsOut(uint256 amountIn, address[] calldata)
+        external
+        view
+        override
+        returns (uint256[] memory amounts)
+    {
+        require(amountIn != 0, "INVALID_AMOUNT_IN");
+        amounts = new uint256[](2);
+        amounts[0] = amountIn;
+
+        IBasketHandler handler = ETH_PLUS.main().basketHandler();
+        (, bytes memory data) = address(this).staticcall(
+            abi.encodeWithSignature("getETHPlusRedemptionQuantities(uint256)", amountIn)
+        );
+        uint256[] memory quantities = abi.decode(data, (uint256[]));
+
+        {
+            amounts[1] += RETH.getEthValue(quantities[2]);
+        }
+
+        {
+            uint256 stEthAmt = WSTETH.getStETHByWstETH(quantities[1]);
+
+            amounts[1] += CURVE_ETHSTETH_STABLE_SWAP.get_dy(1, 0, stEthAmt);
+        }
+
+        {
+            uint256 frxEthAmt = SFRXETH.convertToAssets(quantities[0]);
+            amounts[1] += CURVE_FRXETH_WETH.get_dy(1, 0, frxEthAmt);
+        }
+
+        return amounts;
+    }
+
     function swapExactTokensForETH(
         uint256 amountIn,
         uint256 amountOutMin,
-        // is ignored, but can be empty
+        // is ignored so can be both empty, or token path, or anything
         // solhint-disable-next-line unused-ignore
         address[] calldata,
         address to,
         uint256 deadline
-    ) external returns (uint256[] memory amounts) {
-        require(deadline >= block.timestamp, "deadline");
+    ) external override returns (uint256[] memory amounts) {
+        // solhint-disable-next-line custom-errors
+        require(deadline >= block.timestamp, "DEADLINE");
+        require(to != address(0), "INVALID_TO");
+        require(amountIn != 0, "INVALID_AMOUNT_IN");
         ETH_PLUS.transferFrom(msg.sender, address(this), amountIn);
         ETH_PLUS.redeem(ETH_PLUS.balanceOf(address(this)));
 
         // reth -> eth
-        {
-            uint256 rethBalance = RETH.balanceOf(address(this));
-            (uint256[2] memory portions, uint256 expectedETHOut) = RETH_ROUTER.optimiseSwapFrom(
-                rethBalance,
-                2
-            );
-            RETH.approve(address(rethRouter), rethBalance);
-            RETH_ROUTER.swapFrom(
-                portions[0],
-                portions[1],
-                expectedETHOut,
-                expectedETHOut,
-                rethBalance
-            );
-        }
+        RETH.burn(RETH.balanceOf(address(this)));
 
         // wsteth -> eth
         {
             WSTETH.unwrap(WSTETH.balanceOf(address(this)));
             uint256 stethBalance = STETH.balanceOf(address(this));
             STETH.approve(address(CURVE_ETHSTETH_STABLE_SWAP), stethBalance);
-            CURVE_ETHSTETH_STABLE_SWAP.exchange(1, 0, sfrxethBalance, 0);
+            CURVE_ETHSTETH_STABLE_SWAP.exchange(1, 0, stethBalance, 0);
         }
 
         // sfrxeth -> eth
@@ -135,15 +200,21 @@ contract EthPlusIntoEth is IUniswapV2Like {
             FRXETH.approve(address(CURVE_FRXETH_WETH), frxethBalance);
 
             // frxeth -> weth
-            CURVE_FRXETH_WETH.exchange(1, 0, frxethBalance, 0);
+            CURVE_FRXETH_WETH.exchange(1, 0, frxethBalance, 0, address(this));
 
             // weth -> eth
             WETH.withdraw(WETH.balanceOf(address(this)));
         }
+        amounts = new uint256[](2);
+        amounts[0] = amountIn;
+        amounts[1] = address(this).balance;
+
+        // solhint-disable-next-line custom-errors
+        require(address(this).balance >= amountOutMin, "INSUFFICIENT_OUTPUT_AMOUNT");
+        (bool success, ) = to.call{ value: address(this).balance }("");
 
         // solhint-disable-next-line custom-errors
-        require(this.balance >= amountOutMin, "INSUFFICIENT_OUTPUT_AMOUNT");
-        this.transfer(to, this.balance);
+        require(success, "ETH_TRANSFER_FAILED");
     }
 
     receive() external payable {}
diff --git a/test/rtoken-redemptions/EthPlusIntoEth.test.ts b/test/rtoken-redemptions/EthPlusIntoEth.test.ts
new file mode 100644
index 0000000000..2adc5a4e92
--- /dev/null
+++ b/test/rtoken-redemptions/EthPlusIntoEth.test.ts
@@ -0,0 +1,60 @@
+import { setBalance } from '@nomicfoundation/hardhat-network-helpers'
+import { EthPlusIntoEth } from '@typechain/EthPlusIntoEth'
+import { IERC20 } from '@typechain/IERC20'
+import { formatEther, parseEther } from 'ethers/lib/utils'
+import hardhat, { ethers } from 'hardhat'
+import { whileImpersonating } from '../utils/impersonation'
+import { forkRpcs, Network } from '#/utils/fork'
+import { useEnv } from '#/utils/env'
+import { expect } from 'chai'
+
+describe('EthPlusIntoEth', () => {
+  it('swapExactTokensForETH', async () => {
+    await hardhat.network.provider.request({
+      method: 'hardhat_reset',
+      params: [
+        {
+          forking: {
+            jsonRpcUrl: forkRpcs[useEnv('FORK_NETWORK', 'mainnet') as Network],
+            blockNumber: 20190000,
+          },
+        },
+      ],
+    })
+
+    await whileImpersonating('0x7cc1bfab73be4e02bb53814d1059a98cf7e49644', async (signer) => {
+      await setBalance('0x7cc1bfab73be4e02bb53814d1059a98cf7e49644', parseEther('100'))
+
+      const ethPlusToETH: EthPlusIntoEth = (await ethers.deployContract(
+        'EthPlusIntoEth',
+        signer
+      )) as any
+
+      const reth: IERC20 = await ethers.getContractAt(
+        'IERC20Metadata',
+        '0xE72B141DF173b999AE7c1aDcbF60Cc9833Ce56a8',
+        signer
+      )
+      await reth.approve(ethPlusToETH.address, ethers.utils.parseEther('1'))
+
+      const simuOutput = await ethPlusToETH.callStatic.getAmountsOut(ethers.utils.parseEther('1'), [], {
+        gasLimit: 10_000_000n,
+      })
+
+      const realOutput = await ethPlusToETH.callStatic.swapExactTokensForETH(
+        ethers.utils.parseEther('1'),
+        0,
+        [],
+        '0x7cc1bfab73be4e02bb53814d1059a98cf7e49644',
+        Math.floor(Date.now() / 1000) + 10000,
+        {
+          gasLimit: 10_000_000n,
+        }
+      )
+      
+      expect(
+        Math.abs(parseFloat(formatEther(simuOutput[1].sub(realOutput[1]))))
+      ).to.be.lt(0.00001)
+    })
+  })
+})

From d3569b5251d96984b0bda4ff118c1cae1daef230 Mon Sep 17 00:00:00 2001
From: Jan <mig@jankjr.dk>
Date: Thu, 18 Jul 2024 16:07:41 +0200
Subject: [PATCH 3/8] Handle case where you can't withdraw RETH from the pool

---
 contracts/facade/redeem/EthPlusIntoEth.sol    | 166 ++++++++++++++++--
 .../rtoken-redemptions/EthPlusIntoEth.test.ts | 148 ++++++++++++----
 2 files changed, 265 insertions(+), 49 deletions(-)

diff --git a/contracts/facade/redeem/EthPlusIntoEth.sol b/contracts/facade/redeem/EthPlusIntoEth.sol
index 948d2034f2..f43c158a28 100644
--- a/contracts/facade/redeem/EthPlusIntoEth.sol
+++ b/contracts/facade/redeem/EthPlusIntoEth.sol
@@ -37,6 +37,52 @@ interface IRETHRouter {
         returns (uint256[2] memory portions, uint256 amountOut);
 }
 
+interface IAsset {
+    // solhint-disable-previous-line no-empty-blocks
+}
+
+interface IVault {
+    enum SwapKind {
+        GIVEN_IN,
+        GIVEN_OUT
+    }
+    struct FundManagement {
+        address sender;
+        bool fromInternalBalance;
+        address payable recipient;
+        bool toInternalBalance;
+    }
+    struct SingleSwap {
+        bytes32 poolId;
+        SwapKind kind;
+        IAsset assetIn;
+        IAsset assetOut;
+        uint256 amount;
+        bytes userData;
+    }
+    struct BatchSwapStep {
+        bytes32 poolId;
+        uint256 assetInIndex;
+        uint256 assetOutIndex;
+        uint256 amount;
+        bytes userData;
+    }
+
+    function swap(
+        SingleSwap memory singleSwap,
+        FundManagement memory funds,
+        uint256 limit,
+        uint256 deadline
+    ) external payable returns (uint256);
+
+    function queryBatchSwap(
+        SwapKind kind,
+        BatchSwapStep[] memory swaps,
+        IAsset[] memory assets,
+        FundManagement memory funds
+    ) external returns (int256[] memory assetDeltas);
+}
+
 interface IWSTETH is IERC20 {
     function unwrap(uint256 _wstETHAmount) external returns (uint256);
 
@@ -61,6 +107,8 @@ interface ICurveETHstETHStableSwap {
 interface IRETH is IERC20 {
     function burn(uint256 rethAmt) external;
 
+    function getTotalCollateral() external view returns (uint256);
+
     function getEthValue(uint256 rethAmt) external view returns (uint256);
 }
 
@@ -94,7 +142,7 @@ interface IUniswapV2Like {
         uint256 amountIn,
         // is ignored, can be empty
         address[] calldata path
-    ) external view returns (uint256[] memory amounts);
+    ) external returns (uint256[] memory amounts);
 }
 
 /** Small utility contract to swap ETH+ for ETH by redeeming ETH+ and swapping.
@@ -102,6 +150,10 @@ interface IUniswapV2Like {
 contract EthPlusIntoEth is IUniswapV2Like {
     using SafeERC20 for IERC20;
 
+    IVault private constant BALANCER_VAULT = IVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8);
+    bytes32 private constant BALANCER_POOL_ID =
+        0x1e19cf2d73a72ef1332c882f20534b6519be0276000200000000000000000112;
+
     IRToken private constant ETH_PLUS = IRToken(0xE72B141DF173b999AE7c1aDcbF60Cc9833Ce56a8);
 
     IRETH private constant RETH = IRETH(0xae78736Cd615f374D3085123A210448E74Fc6393);
@@ -121,7 +173,28 @@ contract EthPlusIntoEth is IUniswapV2Like {
     ICurveStableSwap private constant CURVE_FRXETH_WETH =
         ICurveStableSwap(0x9c3B46C0Ceb5B9e304FCd6D88Fc50f7DD24B31Bc);
 
-    function getETHPlusRedemptionQuantities(uint256 amt) external returns (uint256[] memory) {
+    function makeBalancerFunds() internal view returns (IVault.FundManagement memory) {
+        IVault.FundManagement memory fundManagement;
+        fundManagement.sender = address(this);
+        fundManagement.recipient = payable(address(this));
+        fundManagement.fromInternalBalance = false;
+        fundManagement.toInternalBalance = false;
+        return fundManagement;
+    }
+
+    function balancerSwap(uint256 _amount) private {
+        IVault.SingleSwap memory swap;
+        swap.poolId = BALANCER_POOL_ID;
+        swap.kind = IVault.SwapKind.GIVEN_IN;
+        swap.assetIn = IAsset(address(RETH));
+        swap.assetOut = IAsset(address(WETH));
+        swap.amount = _amount;
+        IVault.FundManagement memory funds = makeBalancerFunds();
+        IERC20(RETH).safeApprove(address(BALANCER_VAULT), _amount);
+        BALANCER_VAULT.swap(swap, funds, 0, block.timestamp);
+    }
+
+    function getETHPlusRedemptionQuantities(uint256 amt) external view returns (uint256[] memory) {
         IBasketHandler handler = ETH_PLUS.main().basketHandler();
         uint256 supply = ETH_PLUS.totalSupply();
         (, uint256[] memory quantities) = handler.quote(
@@ -131,9 +204,49 @@ contract EthPlusIntoEth is IUniswapV2Like {
         return quantities;
     }
 
+    function calculateRETHPortions(uint256 amountIn)
+        internal
+        view
+        returns (uint256 toBurn, uint256 toTrade)
+    {
+        uint256 collateralAvailable = RETH.getTotalCollateral();
+
+        if (amountIn > collateralAvailable) {
+            toBurn = collateralAvailable;
+            toTrade = amountIn - collateralAvailable;
+        } else {
+            toBurn = amountIn;
+            toTrade = 0;
+        }
+        return (toBurn, toTrade);
+    }
+
+    function getBalancerQuote(uint256 _amount) internal returns (uint256) {
+        IAsset[] memory assets = new IAsset[](2);
+        assets[0] = IAsset(address(RETH));
+        assets[1] = IAsset(address(WETH));
+
+        IVault.BatchSwapStep[] memory balancerSwapStep = new IVault.BatchSwapStep[](1);
+        balancerSwapStep[0].poolId = BALANCER_POOL_ID;
+        balancerSwapStep[0].amount = _amount;
+        balancerSwapStep[0].assetInIndex = 0;
+        balancerSwapStep[0].assetOutIndex = 1;
+        balancerSwapStep[0].userData = new bytes(0);
+
+        IVault.FundManagement memory funds;
+
+        int256[] memory out = BALANCER_VAULT.queryBatchSwap(
+            IVault.SwapKind.GIVEN_IN,
+            balancerSwapStep,
+            assets,
+            funds
+        );
+
+        return uint256(-out[1]);
+    }
+
     function getAmountsOut(uint256 amountIn, address[] calldata)
         external
-        view
         override
         returns (uint256[] memory amounts)
     {
@@ -141,14 +254,23 @@ contract EthPlusIntoEth is IUniswapV2Like {
         amounts = new uint256[](2);
         amounts[0] = amountIn;
 
-        IBasketHandler handler = ETH_PLUS.main().basketHandler();
         (, bytes memory data) = address(this).staticcall(
             abi.encodeWithSignature("getETHPlusRedemptionQuantities(uint256)", amountIn)
         );
         uint256[] memory quantities = abi.decode(data, (uint256[]));
 
         {
-            amounts[1] += RETH.getEthValue(quantities[2]);
+            (uint256 toBurn, uint256 toTrade) = calculateRETHPortions(quantities[2]);
+
+            if (toBurn > 0) {
+                uint256 burnQuote = RETH.getEthValue(toBurn);
+                amounts[1] += burnQuote;
+            }
+
+            if (toTrade > 0) {
+                uint256 balQuote = getBalancerQuote(toTrade);
+                amounts[1] += balQuote;
+            }
         }
 
         {
@@ -165,6 +287,16 @@ contract EthPlusIntoEth is IUniswapV2Like {
         return amounts;
     }
 
+    function safeTransferETH(address to, uint256 amount) internal {
+        /// @solidity memory-safe-assembly
+        assembly {
+            if iszero(call(gas(), to, amount, codesize(), 0x00, codesize(), 0x00)) {
+                mstore(0x00, 0xb12d13eb) // `ETHTransferFailed()`.
+                revert(0x1c, 0x04)
+            }
+        }
+    }
+
     function swapExactTokensForETH(
         uint256 amountIn,
         uint256 amountOutMin,
@@ -182,7 +314,18 @@ contract EthPlusIntoEth is IUniswapV2Like {
         ETH_PLUS.redeem(ETH_PLUS.balanceOf(address(this)));
 
         // reth -> eth
-        RETH.burn(RETH.balanceOf(address(this)));
+        {
+            (uint256 toBurn, uint256 toTrade) = calculateRETHPortions(
+                RETH.balanceOf(address(this))
+            );
+
+            if (toBurn > 0) {
+                RETH.burn(toBurn);
+            }
+            if (toTrade > 0) {
+                balancerSwap(toTrade);
+            }
+        }
 
         // wsteth -> eth
         {
@@ -201,20 +344,17 @@ contract EthPlusIntoEth is IUniswapV2Like {
 
             // frxeth -> weth
             CURVE_FRXETH_WETH.exchange(1, 0, frxethBalance, 0, address(this));
-
-            // weth -> eth
-            WETH.withdraw(WETH.balanceOf(address(this)));
         }
+
+        // weth -> eth
+        WETH.withdraw(WETH.balanceOf(address(this)));
         amounts = new uint256[](2);
         amounts[0] = amountIn;
         amounts[1] = address(this).balance;
 
-        // solhint-disable-next-line custom-errors
         require(address(this).balance >= amountOutMin, "INSUFFICIENT_OUTPUT_AMOUNT");
-        (bool success, ) = to.call{ value: address(this).balance }("");
 
-        // solhint-disable-next-line custom-errors
-        require(success, "ETH_TRANSFER_FAILED");
+        safeTransferETH(to, amounts[1]);
     }
 
     receive() external payable {}
diff --git a/test/rtoken-redemptions/EthPlusIntoEth.test.ts b/test/rtoken-redemptions/EthPlusIntoEth.test.ts
index 2adc5a4e92..e757b7425b 100644
--- a/test/rtoken-redemptions/EthPlusIntoEth.test.ts
+++ b/test/rtoken-redemptions/EthPlusIntoEth.test.ts
@@ -1,4 +1,4 @@
-import { setBalance } from '@nomicfoundation/hardhat-network-helpers'
+import { loadFixture, setBalance, setCode } from '@nomicfoundation/hardhat-network-helpers'
 import { EthPlusIntoEth } from '@typechain/EthPlusIntoEth'
 import { IERC20 } from '@typechain/IERC20'
 import { formatEther, parseEther } from 'ethers/lib/utils'
@@ -7,54 +7,130 @@ import { whileImpersonating } from '../utils/impersonation'
 import { forkRpcs, Network } from '#/utils/fork'
 import { useEnv } from '#/utils/env'
 import { expect } from 'chai'
+import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'
+const ETH_PLUS_WHALE = '0xc5C75cAF067Ae899a7EC10b86b5aB38C13879388'
 
-describe('EthPlusIntoEth', () => {
-  it('swapExactTokensForETH', async () => {
-    await hardhat.network.provider.request({
-      method: 'hardhat_reset',
-      params: [
-        {
-          forking: {
-            jsonRpcUrl: forkRpcs[useEnv('FORK_NETWORK', 'mainnet') as Network],
-            blockNumber: 20190000,
-          },
+const loader = async () => {
+  await hardhat.network.provider.request({
+    method: 'hardhat_reset',
+    params: [
+      {
+        forking: {
+          jsonRpcUrl: forkRpcs[useEnv('FORK_NETWORK', 'mainnet') as Network],
+          blockNumber: 20334000,
         },
-      ],
-    })
+      },
+    ],
+  })
 
-    await whileImpersonating('0x7cc1bfab73be4e02bb53814d1059a98cf7e49644', async (signer) => {
-      await setBalance('0x7cc1bfab73be4e02bb53814d1059a98cf7e49644', parseEther('100'))
+  await hardhat.network.provider.request({
+    method: 'hardhat_impersonateAccount',
+    params: [ETH_PLUS_WHALE],
+  })
+  const signer = await ethers.getSigner(ETH_PLUS_WHALE)
+  await setBalance(ETH_PLUS_WHALE, parseEther('10000.0'))
+  const ethPlusToETH = await (await ethers.deployContract('EthPlusIntoEth', signer)).deployed()
 
-      const ethPlusToETH: EthPlusIntoEth = (await ethers.deployContract(
-        'EthPlusIntoEth',
-        signer
-      )) as any
+  const reth = await ethers.getContractAt(
+    'IERC20Metadata',
+    '0xE72B141DF173b999AE7c1aDcbF60Cc9833Ce56a8',
+    signer
+  )
+  await (await reth.approve(ethPlusToETH.address, ethers.utils.parseEther('10000'))).wait(0)
 
-      const reth: IERC20 = await ethers.getContractAt(
-        'IERC20Metadata',
-        '0xE72B141DF173b999AE7c1aDcbF60Cc9833Ce56a8',
-        signer
-      )
-      await reth.approve(ethPlusToETH.address, ethers.utils.parseEther('1'))
+  return { ethPlusToETH, reth, signer }
+}
+const runTestScenario = async (
+  body: (state: {
+    signer: SignerWithAddress
+    ethPlusToETH: EthPlusIntoEth
+    reth: IERC20
+  }) => Promise<void>
+) => {
+  const { ethPlusToETH, reth, signer } = await loadFixture(loader)
 
-      const simuOutput = await ethPlusToETH.callStatic.getAmountsOut(ethers.utils.parseEther('1'), [], {
-        gasLimit: 10_000_000n,
-      })
+  await body({
+    ethPlusToETH: ethPlusToETH as EthPlusIntoEth,
+    signer,
+    reth,
+  })
+}
 
-      const realOutput = await ethPlusToETH.callStatic.swapExactTokensForETH(
+describe('EthPlusIntoEth', () => {
+  it('swapExactTokensForETH and getAmountsOut are consistent (enough)', async () => {
+    await runTestScenario(async ({ ethPlusToETH }) => {
+      const simuOutput = await ethPlusToETH.callStatic.getAmountsOut(
         ethers.utils.parseEther('1'),
-        0,
         [],
-        '0x7cc1bfab73be4e02bb53814d1059a98cf7e49644',
-        Math.floor(Date.now() / 1000) + 10000,
         {
           gasLimit: 10_000_000n,
         }
       )
-      
-      expect(
-        Math.abs(parseFloat(formatEther(simuOutput[1].sub(realOutput[1]))))
-      ).to.be.lt(0.00001)
+      expect(parseFloat(formatEther(simuOutput[1]))).to.be.gt(1.017)
+
+      const ethBalBefore = await ethers.provider.getBalance(ETH_PLUS_WHALE)
+
+      await ethPlusToETH.swapExactTokensForETH(
+        ethers.utils.parseEther('1'),
+        0,
+        [],
+        ETH_PLUS_WHALE,
+        0xffffffffffffffffn
+      )
+
+      const ethBalAfter = await ethers.provider.getBalance(ETH_PLUS_WHALE)
+      expect(ethBalAfter).to.be.gt(ethBalBefore)
+    })
+  })
+
+  it('Handles 1000 RETH', async () => {
+    await runTestScenario(async ({ ethPlusToETH }) => {
+      const ethBalBefore = await ethers.provider.getBalance(ETH_PLUS_WHALE)
+
+      await ethPlusToETH.swapExactTokensForETH(
+        ethers.utils.parseEther('1000'),
+        0,
+        [],
+        ETH_PLUS_WHALE,
+        0xffffffffffffffffn
+      )
+
+      const ethBalAfter = await ethers.provider.getBalance(ETH_PLUS_WHALE)
+      expect(parseFloat(formatEther(ethBalAfter.sub(ethBalBefore)))).to.be.gt(1017.67)
+    })
+  })
+
+  it('Handles 2000 RETH', async () => {
+    await runTestScenario(async ({ ethPlusToETH }) => {
+      const ethBalBefore = await ethers.provider.getBalance(ETH_PLUS_WHALE)
+      await ethPlusToETH.swapExactTokensForETH(
+        ethers.utils.parseEther('2000'),
+        0,
+        [],
+        ETH_PLUS_WHALE,
+        0xffffffffffffffffn
+      )
+
+      const ethBalAfter = await ethers.provider.getBalance(ETH_PLUS_WHALE)
+
+      expect(parseFloat(formatEther(ethBalAfter.sub(ethBalBefore)))).to.be.gt(2035.25)
+    })
+  })
+
+  it('Handles 3000 RETH', async () => {
+    await runTestScenario(async ({ ethPlusToETH }) => {
+      const ethBalBefore = await ethers.provider.getBalance(ETH_PLUS_WHALE)
+      await ethPlusToETH.swapExactTokensForETH(
+        ethers.utils.parseEther('3000'),
+        0,
+        [],
+        ETH_PLUS_WHALE,
+        0xffffffffffffffffn
+      )
+
+      const ethBalAfter = await ethers.provider.getBalance(ETH_PLUS_WHALE)
+
+      expect(parseFloat(formatEther(ethBalAfter.sub(ethBalBefore)))).to.be.gt(3052)
     })
   })
 })

From 27b517720d942289c17c2c44eb325daf4f6bcc17 Mon Sep 17 00:00:00 2001
From: Jan <mig@jankjr.dk>
Date: Thu, 18 Jul 2024 20:57:11 +0200
Subject: [PATCH 4/8] Add upgradable

---
 .../{facade => }/redeem/EthPlusIntoEth.sol    | 27 ++++++++++---
 hardhat.config.ts                             | 38 +++++++------------
 .../redeem/deploy-ethplus-to-eth.ts           | 32 ++++++++++++++++
 tasks/index.ts                                |  1 +
 .../rtoken-redemptions/EthPlusIntoEth.test.ts |  8 +++-
 utils/env.ts                                  |  1 +
 6 files changed, 76 insertions(+), 31 deletions(-)
 rename contracts/{facade => }/redeem/EthPlusIntoEth.sol (91%)
 create mode 100644 tasks/deployment/redeem/deploy-ethplus-to-eth.ts

diff --git a/contracts/facade/redeem/EthPlusIntoEth.sol b/contracts/redeem/EthPlusIntoEth.sol
similarity index 91%
rename from contracts/facade/redeem/EthPlusIntoEth.sol
rename to contracts/redeem/EthPlusIntoEth.sol
index f43c158a28..c295156dec 100644
--- a/contracts/facade/redeem/EthPlusIntoEth.sol
+++ b/contracts/redeem/EthPlusIntoEth.sol
@@ -1,12 +1,13 @@
 // SPDX-License-Identifier: BlueOak-1.0.0
 pragma solidity 0.8.19;
-
+import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
 import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
 import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
 import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol";
-import { IBasketHandler } from "../../interfaces/IBasketHandler.sol";
-import { IRToken } from "../../interfaces/IRToken.sol";
-import { RoundingMode, FixLib } from "../../libraries/Fixed.sol";
+import { IBasketHandler } from "../interfaces/IBasketHandler.sol";
+import { IRToken } from "../interfaces/IRToken.sol";
+import { RoundingMode, FixLib } from "../libraries/Fixed.sol";
 
 interface IWETH is IERC20 {
     function withdraw(uint256 wad) external;
@@ -147,7 +148,7 @@ interface IUniswapV2Like {
 
 /** Small utility contract to swap ETH+ for ETH by redeeming ETH+ and swapping.
  */
-contract EthPlusIntoEth is IUniswapV2Like {
+contract EthPlusIntoEth is IUniswapV2Like, UUPSUpgradeable, OwnableUpgradeable {
     using SafeERC20 for IERC20;
 
     IVault private constant BALANCER_VAULT = IVault(0xBA12222222228d8Ba445958a75a0704d566BF2C8);
@@ -173,6 +174,11 @@ contract EthPlusIntoEth is IUniswapV2Like {
     ICurveStableSwap private constant CURVE_FRXETH_WETH =
         ICurveStableSwap(0x9c3B46C0Ceb5B9e304FCd6D88Fc50f7DD24B31Bc);
 
+    function initialize() public initializer {
+        __Ownable_init();
+        __UUPSUpgradeable_init();
+    }
+
     function makeBalancerFunds() internal view returns (IVault.FundManagement memory) {
         IVault.FundManagement memory fundManagement;
         fundManagement.sender = address(this);
@@ -358,4 +364,15 @@ contract EthPlusIntoEth is IUniswapV2Like {
     }
 
     receive() external payable {}
+
+    // === Upgradeability ===
+    // solhint-disable-next-line no-empty-blocks
+    function _authorizeUpgrade(address newImplementation) internal override onlyOwner {}
+
+    /**
+     * @dev This empty reserved space is put in place to allow future versions to add new
+     * variables without shifting down storage in the inheritance chain.
+     * See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
+     */
+    uint256[49] private __gap;
 }
diff --git a/hardhat.config.ts b/hardhat.config.ts
index 0e9f812b2e..40d26cdecd 100644
--- a/hardhat.config.ts
+++ b/hardhat.config.ts
@@ -28,11 +28,15 @@ const BASE_RPC_URL = useEnv('BASE_RPC_URL')
 const ARBITRUM_SEPOLIA_RPC_URL = useEnv('ARBITRUM_SEPOLIA_RPC_URL')
 const ARBITRUM_RPC_URL = useEnv('ARBITRUM_RPC_URL')
 const MNEMONIC = useEnv('MNEMONIC') || 'test test test test test test test test test test test junk'
+const PRIVATE_KEY = useEnv('PRIVATE_KEY')
 const TIMEOUT = useEnv('SLOW') ? 6_000_000 : 600_000
 
 const src_dir = `./contracts/${useEnv('PROTO')}`
 const settings = useEnv('NO_OPT') ? {} : { optimizer: { enabled: true, runs: 200 } }
 
+const accounts = PRIVATE_KEY != null ? [PRIVATE_KEY] : {
+  mnemonic: MNEMONIC,
+}
 const config: HardhatUserConfig = {
   defaultNetwork: 'hardhat',
   networks: {
@@ -40,9 +44,9 @@ const config: HardhatUserConfig = {
       // network for tests/in-process stuff
       forking: useEnv('FORK')
         ? {
-            url: forkRpcs[useEnv('FORK_NETWORK', 'mainnet') as Network],
-            blockNumber: Number(useEnv(`FORK_BLOCK`, forkBlockNumber['default'].toString())),
-          }
+          url: forkRpcs[useEnv('FORK_NETWORK', 'mainnet') as Network],
+          blockNumber: Number(useEnv(`FORK_BLOCK`, forkBlockNumber['default'].toString())),
+        }
         : undefined,
       gas: 0x1ffffffff,
       blockGasLimit: 0x1fffffffffffff,
@@ -60,54 +64,40 @@ const config: HardhatUserConfig = {
     goerli: {
       chainId: 5,
       url: GOERLI_RPC_URL,
-      accounts: {
-        mnemonic: MNEMONIC,
-      },
+      accounts,
     },
     'base-goerli': {
       chainId: 84531,
       url: BASE_GOERLI_RPC_URL,
-      accounts: {
-        mnemonic: MNEMONIC,
-      },
+      accounts,
     },
     base: {
       chainId: 8453,
       url: BASE_RPC_URL,
-      accounts: {
-        mnemonic: MNEMONIC,
-      },
+      accounts,
     },
     mainnet: {
       chainId: 1,
       url: MAINNET_RPC_URL,
-      accounts: {
-        mnemonic: MNEMONIC,
-      },
+      accounts,
       // gasPrice: 30_000_000_000,
       gasMultiplier: 2, // 100% buffer; seen failures on RToken deployment and asset refreshes otherwise
     },
     arbitrum: {
       chainId: 42161,
       url: ARBITRUM_RPC_URL,
-      accounts: {
-        mnemonic: MNEMONIC,
-      },
+      accounts,
       gasMultiplier: 2, // 100% buffer; seen failures on RToken deployment and asset refreshes otherwise
     },
     'arbitrum-sepolia': {
       chainId: 421614,
       url: ARBITRUM_SEPOLIA_RPC_URL,
-      accounts: {
-        mnemonic: MNEMONIC,
-      },
+      accounts,
     },
     tenderly: {
       chainId: 3,
       url: TENDERLY_RPC_URL,
-      accounts: {
-        mnemonic: MNEMONIC,
-      },
+      accounts,
       // gasPrice: 10_000_000_000,
       gasMultiplier: 2, // 100% buffer; seen failures on RToken deployment and asset refreshes otherwise
     },
diff --git a/tasks/deployment/redeem/deploy-ethplus-to-eth.ts b/tasks/deployment/redeem/deploy-ethplus-to-eth.ts
new file mode 100644
index 0000000000..969754dc92
--- /dev/null
+++ b/tasks/deployment/redeem/deploy-ethplus-to-eth.ts
@@ -0,0 +1,32 @@
+import { task } from 'hardhat/config'
+
+task('deploy-redeem-ethplus', 'Deploys the EthPlusIntoEth contract, it offers an easy to use UniswapV2 like interface for external parties to integrate against').setAction(async (_, hre) => {
+  const [deployer] = await hre.ethers.getSigners()
+
+  console.log(
+    `Deploying EthPlusIntoEth contract to network ${hre.network.name} with deployer account ${deployer.address}...`
+  )
+
+  // Deploy the EthPlusIntoEth contract
+  const EthPlusIntoEthFactory = await hre.ethers.getContractFactory('EthPlusIntoEth')
+  const ethPlusIntoEth = await hre.upgrades.deployProxy(EthPlusIntoEthFactory, [], {
+    kind: 'uups',
+    redeployImplementation: 'onchange',
+  })
+  await ethPlusIntoEth.deployed()
+
+  console.log(`Deployed EthPlusIntoEth to ${hre.network.name}:
+      EthPlusIntoEth: ${ethPlusIntoEth.address}`)
+
+  /** ******************** Verify EthPlusIntoEth ****************************************/
+  console.time('Verifying EthPlusIntoEth Implementation')
+  await hre.run('verify:verify', {
+    address: ethPlusIntoEth.address,
+    constructorArguments: [],
+    contract: "contracts/redeem/EthPlusIntoEth.sol:EthPlusIntoEth",
+  })
+  console.timeEnd('Verifying EthPlusIntoEth Implementation')
+
+  console.log('verified')
+
+})
diff --git a/tasks/index.ts b/tasks/index.ts
index 6fba90bae8..41480aac03 100644
--- a/tasks/index.ts
+++ b/tasks/index.ts
@@ -21,6 +21,7 @@ import './deployment/empty-wallet'
 import './deployment/cancel-tx'
 import './deployment/deploy-spell'
 import './deployment/sign-msg'
+import './deployment/redeem/deploy-ethplus-to-eth'
 import './deployment/get-addresses'
 import './deployment/deploy-governor-anastasius'
 import './deployment/deploy-timelock'
diff --git a/test/rtoken-redemptions/EthPlusIntoEth.test.ts b/test/rtoken-redemptions/EthPlusIntoEth.test.ts
index e757b7425b..7fef53edb7 100644
--- a/test/rtoken-redemptions/EthPlusIntoEth.test.ts
+++ b/test/rtoken-redemptions/EthPlusIntoEth.test.ts
@@ -3,7 +3,6 @@ import { EthPlusIntoEth } from '@typechain/EthPlusIntoEth'
 import { IERC20 } from '@typechain/IERC20'
 import { formatEther, parseEther } from 'ethers/lib/utils'
 import hardhat, { ethers } from 'hardhat'
-import { whileImpersonating } from '../utils/impersonation'
 import { forkRpcs, Network } from '#/utils/fork'
 import { useEnv } from '#/utils/env'
 import { expect } from 'chai'
@@ -29,7 +28,12 @@ const loader = async () => {
   })
   const signer = await ethers.getSigner(ETH_PLUS_WHALE)
   await setBalance(ETH_PLUS_WHALE, parseEther('10000.0'))
-  const ethPlusToETH = await (await ethers.deployContract('EthPlusIntoEth', signer)).deployed()
+  const ethPlusToETHFactory = await ethers.getContractFactory('EthPlusIntoEth')
+  const ethPlusToETH = (
+    await hardhat.upgrades.deployProxy(ethPlusToETHFactory, [], {
+      kind: 'uups',
+    })
+  ).connect(signer)
 
   const reth = await ethers.getContractAt(
     'IERC20Metadata',
diff --git a/utils/env.ts b/utils/env.ts
index 8af0009843..7ff4f7450f 100644
--- a/utils/env.ts
+++ b/utils/env.ts
@@ -31,6 +31,7 @@ type IEnvVars =
   | 'FORK_NETWORK'
   | 'FORK_BLOCK'
   | 'FORCE_WHALE_REFRESH'
+  | 'PRIVATE_KEY'
 
 export function useEnv(key: IEnvVars | IEnvVars[], _default = ''): string {
   if (typeof key === 'string') {

From f97651974ab9b290b21025244bb07c70233744c9 Mon Sep 17 00:00:00 2001
From: Jan <mig@jankjr.dk>
Date: Thu, 18 Jul 2024 21:18:46 +0200
Subject: [PATCH 5/8] copy tweak

---
 tasks/deployment/redeem/deploy-ethplus-to-eth.ts | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/tasks/deployment/redeem/deploy-ethplus-to-eth.ts b/tasks/deployment/redeem/deploy-ethplus-to-eth.ts
index 969754dc92..1243bf95da 100644
--- a/tasks/deployment/redeem/deploy-ethplus-to-eth.ts
+++ b/tasks/deployment/redeem/deploy-ethplus-to-eth.ts
@@ -1,6 +1,9 @@
 import { task } from 'hardhat/config'
 
-task('deploy-redeem-ethplus', 'Deploys the EthPlusIntoEth contract, it offers an easy to use UniswapV2 like interface for external parties to integrate against').setAction(async (_, hre) => {
+task(
+  'deploy-redeem-ethplus',
+  'Deploys the EthPlusIntoEth contract. It offers a UniswapV2-like interface to exit ETH+ that is fully decentralised'
+).setAction(async (_, hre) => {
   const [deployer] = await hre.ethers.getSigners()
 
   console.log(
@@ -23,10 +26,9 @@ task('deploy-redeem-ethplus', 'Deploys the EthPlusIntoEth contract, it offers an
   await hre.run('verify:verify', {
     address: ethPlusIntoEth.address,
     constructorArguments: [],
-    contract: "contracts/redeem/EthPlusIntoEth.sol:EthPlusIntoEth",
+    contract: 'contracts/redeem/EthPlusIntoEth.sol:EthPlusIntoEth',
   })
   console.timeEnd('Verifying EthPlusIntoEth Implementation')
 
   console.log('verified')
-
 })

From 6f641323cb8e73b527ab123dd035b95e3e17b8b0 Mon Sep 17 00:00:00 2001
From: Jan <mig@jankjr.dk>
Date: Thu, 18 Jul 2024 21:38:48 +0200
Subject: [PATCH 6/8] comments

---
 contracts/redeem/EthPlusIntoEth.sol | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/contracts/redeem/EthPlusIntoEth.sol b/contracts/redeem/EthPlusIntoEth.sol
index c295156dec..5aa718c129 100644
--- a/contracts/redeem/EthPlusIntoEth.sol
+++ b/contracts/redeem/EthPlusIntoEth.sol
@@ -251,6 +251,9 @@ contract EthPlusIntoEth is IUniswapV2Like, UUPSUpgradeable, OwnableUpgradeable {
         return uint256(-out[1]);
     }
 
+    /// @dev Returns the amounts of ETH a given amount of RETH can be redeemed into
+    /// The function is meant be called from off-chain for getting the current quote, or to
+    /// calculate the minAmount to use for the swapExactTokensForETH function
     function getAmountsOut(uint256 amountIn, address[] calldata)
         external
         override
@@ -303,6 +306,7 @@ contract EthPlusIntoEth is IUniswapV2Like, UUPSUpgradeable, OwnableUpgradeable {
         }
     }
 
+    /// @dev Swaps RETH for ETH, RETH must be approved before calling this function
     function swapExactTokensForETH(
         uint256 amountIn,
         uint256 amountOutMin,

From 674db7b4037a175b10a69965960b77ce4bd70e72 Mon Sep 17 00:00:00 2001
From: Jan <mig@jankjr.dk>
Date: Thu, 18 Jul 2024 21:40:08 +0200
Subject: [PATCH 7/8] lint

---
 contracts/redeem/EthPlusIntoEth.sol | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/contracts/redeem/EthPlusIntoEth.sol b/contracts/redeem/EthPlusIntoEth.sol
index 5aa718c129..88266f3b7c 100644
--- a/contracts/redeem/EthPlusIntoEth.sol
+++ b/contracts/redeem/EthPlusIntoEth.sol
@@ -1,7 +1,7 @@
 // SPDX-License-Identifier: BlueOak-1.0.0
 pragma solidity 0.8.19;
-import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
-import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
+import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
 import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
 import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
 import { IERC4626 } from "@openzeppelin/contracts/interfaces/IERC4626.sol";

From 8476abd5c40f13ac289743ac09c675d415e7932d Mon Sep 17 00:00:00 2001
From: Jan <mig@jankjr.dk>
Date: Thu, 18 Jul 2024 21:49:40 +0200
Subject: [PATCH 8/8] deploy Eth+ redemption contract

---
 .openzeppelin/mainnet.json | 108 ++++++++++++++++++++++++++++++++++++-
 1 file changed, 107 insertions(+), 1 deletion(-)

diff --git a/.openzeppelin/mainnet.json b/.openzeppelin/mainnet.json
index 7e50e1dc38..d89b536e24 100644
--- a/.openzeppelin/mainnet.json
+++ b/.openzeppelin/mainnet.json
@@ -1,6 +1,12 @@
 {
   "manifestVersion": "3.2",
-  "proxies": [],
+  "proxies": [
+    {
+      "address": "0x6702c91ffC24fA862B8D9C053d8C3e395c379E1e",
+      "txHash": "0x4af4d98737396c1c4e31768387d7026851b135de3199615f942cbfe72ae154d4",
+      "kind": "uups"
+    }
+  ],
   "impls": {
     "5772f9a12946a693b87839e6ea71a2c52293982e3a7989190fa8d1e93305a58c": {
       "address": "0x143C35bFe04720394eBd18AbECa83eA9D8BEdE2F",
@@ -10166,6 +10172,106 @@
           }
         }
       }
+    },
+    "ae1029b17f53d69f796c3f889c9bb28ba66d7ba84e0b1d7344697cf40207aac2": {
+      "address": "0x0Da349103B71821c688308Ea4477E6A3359d69B1",
+      "txHash": "0x9f2c2745f70a513b5f671dc7d32823a14b7e273d40f4ad878ea2847873cee3d1",
+      "layout": {
+        "solcVersion": "0.8.19",
+        "storage": [
+          {
+            "label": "_initialized",
+            "offset": 0,
+            "slot": "0",
+            "type": "t_uint8",
+            "contract": "Initializable",
+            "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:63",
+            "retypedFrom": "bool"
+          },
+          {
+            "label": "_initializing",
+            "offset": 1,
+            "slot": "0",
+            "type": "t_bool",
+            "contract": "Initializable",
+            "src": "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol:68"
+          },
+          {
+            "label": "__gap",
+            "offset": 0,
+            "slot": "1",
+            "type": "t_array(t_uint256)50_storage",
+            "contract": "ERC1967UpgradeUpgradeable",
+            "src": "@openzeppelin/contracts-upgradeable/proxy/ERC1967/ERC1967UpgradeUpgradeable.sol:169"
+          },
+          {
+            "label": "__gap",
+            "offset": 0,
+            "slot": "51",
+            "type": "t_array(t_uint256)50_storage",
+            "contract": "UUPSUpgradeable",
+            "src": "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol:111"
+          },
+          {
+            "label": "__gap",
+            "offset": 0,
+            "slot": "101",
+            "type": "t_array(t_uint256)50_storage",
+            "contract": "ContextUpgradeable",
+            "src": "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol:40"
+          },
+          {
+            "label": "_owner",
+            "offset": 0,
+            "slot": "151",
+            "type": "t_address",
+            "contract": "OwnableUpgradeable",
+            "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:22"
+          },
+          {
+            "label": "__gap",
+            "offset": 0,
+            "slot": "152",
+            "type": "t_array(t_uint256)49_storage",
+            "contract": "OwnableUpgradeable",
+            "src": "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol:94"
+          },
+          {
+            "label": "__gap",
+            "offset": 0,
+            "slot": "201",
+            "type": "t_array(t_uint256)49_storage",
+            "contract": "EthPlusIntoEth",
+            "src": "contracts/facade/redeem/EthPlusIntoEth.sol:377"
+          }
+        ],
+        "types": {
+          "t_address": {
+            "label": "address",
+            "numberOfBytes": "20"
+          },
+          "t_array(t_uint256)49_storage": {
+            "label": "uint256[49]",
+            "numberOfBytes": "1568"
+          },
+          "t_array(t_uint256)50_storage": {
+            "label": "uint256[50]",
+            "numberOfBytes": "1600"
+          },
+          "t_bool": {
+            "label": "bool",
+            "numberOfBytes": "1"
+          },
+          "t_uint256": {
+            "label": "uint256",
+            "numberOfBytes": "32"
+          },
+          "t_uint8": {
+            "label": "uint8",
+            "numberOfBytes": "1"
+          }
+        }
+      }
     }
   }
 }