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" + } + } + } } } }