Skip to content

Commit

Permalink
Handle case where you can't withdraw RETH from the pool
Browse files Browse the repository at this point in the history
  • Loading branch information
jankjr committed Jul 18, 2024
1 parent 8ad7be2 commit f17d084
Show file tree
Hide file tree
Showing 2 changed files with 155 additions and 15 deletions.
144 changes: 138 additions & 6 deletions contracts/facade/redeem/EthPlusIntoEth.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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);
}

Expand Down Expand Up @@ -94,14 +142,18 @@ 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.
*/
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);
Expand All @@ -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(
Expand All @@ -131,24 +204,73 @@ 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)
{
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 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;
}
}

{
Expand Down Expand Up @@ -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
{
Expand Down
26 changes: 17 additions & 9 deletions test/rtoken-redemptions/EthPlusIntoEth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'), [])

Check failure on line 40 in test/rtoken-redemptions/EthPlusIntoEth.test.ts

View workflow job for this annotation

GitHub Actions / Lint Checks

Replace `ethers.utils.parseEther('1'),·[]` with `⏎········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])));

Check failure on line 49 in test/rtoken-redemptions/EthPlusIntoEth.test.ts

View workflow job for this annotation

GitHub Actions / Lint Checks

Delete `;`

expect(

Check failure on line 51 in test/rtoken-redemptions/EthPlusIntoEth.test.ts

View workflow job for this annotation

GitHub Actions / Lint Checks

Replace `⏎········Math.abs(outNum)⏎······` with `Math.abs(outNum)`
Math.abs(parseFloat(formatEther(simuOutput[1].sub(realOutput[1]))))
Math.abs(outNum)
).to.be.lt(0.00001)

const ethBalBefore = await ethers.provider.getBalance("0x7cc1bfab73be4e02bb53814d1059a98cf7e49644")

Check failure on line 55 in test/rtoken-redemptions/EthPlusIntoEth.test.ts

View workflow job for this annotation

GitHub Actions / Lint Checks

Replace `"0x7cc1bfab73be4e02bb53814d1059a98cf7e49644"` with `⏎········'0x7cc1bfab73be4e02bb53814d1059a98cf7e49644'⏎······`
await ethPlusToETH.swapExactTokensForETH(
ethers.utils.parseEther('1'),
0,
[],
'0x7cc1bfab73be4e02bb53814d1059a98cf7e49644',
Math.floor(Date.now() / 1000) + 10000
)
const ethBalAfter = await ethers.provider.getBalance("0x7cc1bfab73be4e02bb53814d1059a98cf7e49644")

Check failure on line 63 in test/rtoken-redemptions/EthPlusIntoEth.test.ts

View workflow job for this annotation

GitHub Actions / Lint Checks

Replace `"0x7cc1bfab73be4e02bb53814d1059a98cf7e49644"` with `⏎········'0x7cc1bfab73be4e02bb53814d1059a98cf7e49644'⏎······`

expect(ethBalAfter).to.be.gt(ethBalBefore)
})
})
})

0 comments on commit f17d084

Please sign in to comment.