diff --git a/contracts/facade/redeem/EthPlusIntoEth.sol b/contracts/facade/redeem/EthPlusIntoEth.sol index 948d2034f2..b3ef9d2c06 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; + } } { @@ -182,7 +304,17 @@ 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 (toTrade > 0) { + balancerSwap(toTrade); + } + if (toBurn > 0) { + RETH.burn(toBurn); + } + } // wsteth -> eth { diff --git a/test/rtoken-redemptions/EthPlusIntoEth.test.ts b/test/rtoken-redemptions/EthPlusIntoEth.test.ts index 2adc5a4e92..cdd1bfd6ce 100644 --- a/test/rtoken-redemptions/EthPlusIntoEth.test.ts +++ b/test/rtoken-redemptions/EthPlusIntoEth.test.ts @@ -37,24 +37,32 @@ describe('EthPlusIntoEth', () => { ) await reth.approve(ethPlusToETH.address, ethers.utils.parseEther('1')) - const simuOutput = await ethPlusToETH.callStatic.getAmountsOut(ethers.utils.parseEther('1'), [], { - gasLimit: 10_000_000n, - }) + const simuOutput = await ethPlusToETH.callStatic.getAmountsOut(ethers.utils.parseEther('1'), []) const realOutput = await ethPlusToETH.callStatic.swapExactTokensForETH( ethers.utils.parseEther('1'), 0, [], '0x7cc1bfab73be4e02bb53814d1059a98cf7e49644', - Math.floor(Date.now() / 1000) + 10000, - { - gasLimit: 10_000_000n, - } + Math.floor(Date.now() / 1000) + 10000 ) - + const outNum = parseFloat(formatEther(simuOutput[1].sub(realOutput[1]))); + expect( - Math.abs(parseFloat(formatEther(simuOutput[1].sub(realOutput[1])))) + Math.abs(outNum) ).to.be.lt(0.00001) + + const ethBalBefore = await ethers.provider.getBalance("0x7cc1bfab73be4e02bb53814d1059a98cf7e49644") + await ethPlusToETH.swapExactTokensForETH( + ethers.utils.parseEther('1'), + 0, + [], + '0x7cc1bfab73be4e02bb53814d1059a98cf7e49644', + Math.floor(Date.now() / 1000) + 10000 + ) + const ethBalAfter = await ethers.provider.getBalance("0x7cc1bfab73be4e02bb53814d1059a98cf7e49644") + + expect(ethBalAfter).to.be.gt(ethBalBefore) }) }) })