From 3f1b9f8aaa9630987e9b8e89f8bc9542be68815a Mon Sep 17 00:00:00 2001 From: Domen Grabec Date: Wed, 8 Nov 2023 19:06:03 +0100 Subject: [PATCH] add support to withdraw the liquidity when balancer pool is in recovery mode --- .../interfaces/balancer/IBalancerPool.sol | 2 + .../balancer/BalancerMetaPoolStrategy.sol | 34 +++++++++++- contracts/test/fixture/_fixture.js | 13 +++++ .../BalancerComposableStablePool.fork-test.js | 53 +++++++++++++++++++ .../balancerMetaStablePool.fork-test.js | 10 ++++ 5 files changed, 111 insertions(+), 1 deletion(-) diff --git a/contracts/contracts/interfaces/balancer/IBalancerPool.sol b/contracts/contracts/interfaces/balancer/IBalancerPool.sol index d294e0345c..a53dcc9cee 100644 --- a/contracts/contracts/interfaces/balancer/IBalancerPool.sol +++ b/contracts/contracts/interfaces/balancer/IBalancerPool.sol @@ -8,4 +8,6 @@ interface IBalancerPool { external view returns (IRateProvider[] memory providers); + + function inRecoveryMode() external view returns (bool); } diff --git a/contracts/contracts/strategies/balancer/BalancerMetaPoolStrategy.sol b/contracts/contracts/strategies/balancer/BalancerMetaPoolStrategy.sol index 9d3fadba1a..4465ab2039 100644 --- a/contracts/contracts/strategies/balancer/BalancerMetaPoolStrategy.sol +++ b/contracts/contracts/strategies/balancer/BalancerMetaPoolStrategy.sol @@ -8,12 +8,17 @@ pragma solidity ^0.8.0; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { BaseAuraStrategy, BaseBalancerStrategy } from "./BaseAuraStrategy.sol"; import { IBalancerVault } from "../../interfaces/balancer/IBalancerVault.sol"; +import { IBalancerPool } from "../../interfaces/balancer/IBalancerPool.sol"; import { IERC20, InitializableAbstractStrategy } from "../../utils/InitializableAbstractStrategy.sol"; import { StableMath } from "../../utils/StableMath.sol"; contract BalancerMetaPoolStrategy is BaseAuraStrategy { using SafeERC20 for IERC20; using StableMath for uint256; + + // Special ExitKind for all Balancer pools, used in Recovery Mode. + uint256 constant RECOVERY_MODE_EXIT_KIND = 255; + /* For Meta stable pools the enum value should be "2" as it is defined * in the IBalancerVault. From the Metastable pool codebase: * @@ -421,6 +426,18 @@ contract BalancerMetaPoolStrategy is BaseAuraStrategy { * Is only executable by the OToken's Vault or the Governor. */ function withdrawAll() external override onlyVaultOrGovernor nonReentrant { + _withdrawAll(false); + } + + function recoveryModeWithdrawAll() + external + onlyVaultOrGovernor + nonReentrant + { + _withdrawAll(true); + } + + function _withdrawAll(bool isRecoveryModeWithdrawal) internal { // STEP 1 - Withdraw all Balancer Pool Tokens (BPT) from Aura to this strategy contract _lpWithdrawAll(); @@ -441,12 +458,27 @@ contract BalancerMetaPoolStrategy is BaseAuraStrategy { * It is ok to pass an empty minAmountsOut since tilting the pool in any direction * when doing a proportional exit can only be beneficial to the strategy. Since * it will receive more of the underlying tokens for the BPT traded in. + * + * Important when `isRecoveryModeWithdrawal` is true then a special recovery mode exit + * kind is used for a much simpler and more gas efficient exit of the pool. */ bytes memory userData = abi.encode( - balancerExactBptInTokensOutIndex, + isRecoveryModeWithdrawal + ? RECOVERY_MODE_EXIT_KIND + : balancerExactBptInTokensOutIndex, BPTtoWithdraw ); + if (isRecoveryModeWithdrawal) { + /* Older Balancer pools don't support this functionality (e.g. rETH/WETH). In that case the + * transaction will just fail as it should. + */ + require( + IBalancerPool(platformAddress).inRecoveryMode(), + "Pool not in recovery mode" + ); + } + IBalancerVault.ExitPoolRequest memory request = IBalancerVault .ExitPoolRequest(poolAssets, minAmountsOut, userData, false); diff --git a/contracts/test/fixture/_fixture.js b/contracts/test/fixture/_fixture.js index 5b4b3e96ed..17523ff4c5 100644 --- a/contracts/test/fixture/_fixture.js +++ b/contracts/test/fixture/_fixture.js @@ -1022,6 +1022,19 @@ async function balancerFrxETHwstETHeETHFixture( josh ); + /* balancer Gnosis safe authorized account + * Use this Dube query to get relevant transactions: + - https://dune.com/queries/3184026 + */ + const authorizerAddress = "0xa29f61256e948f3fb707b4b3b138c5ccb9ef9888"; + const recoveryModeSigner = await impersonateAndFund(authorizerAddress); + + fixture.enableRecoveryMode = async () => { + await fixture.sfrxETHwstETHrEthBPT + .connect(recoveryModeSigner) + .enableRecoveryMode(); + }; + await setERC20TokenBalance(josh.address, reth, "1000000", hre); await setERC20TokenBalance(josh.address, frxETH, "1000000", hre); await setERC20TokenBalance(josh.address, stETH, "1000000", hre); diff --git a/contracts/test/strategies/BalancerComposableStablePool.fork-test.js b/contracts/test/strategies/BalancerComposableStablePool.fork-test.js index 84963f86a4..67ed4482a5 100644 --- a/contracts/test/strategies/BalancerComposableStablePool.fork-test.js +++ b/contracts/test/strategies/BalancerComposableStablePool.fork-test.js @@ -436,6 +436,59 @@ describe("ForkTest: Balancer ComposableStablePool sfrxETH/wstETH/rETH Strategy", expect(rethBalanceDiff).to.be.gte(await units("15", reth), 1); expect(frxEthBalanceDiff).to.be.gte(await units("15", frxETH), 1); }); + + it("Should be able to withdraw all of pool liquidity in recovery mode", async function () { + const { + oethVault, + stETH, + frxETH, + reth, + balancerSfrxWstRETHStrategy, + enableRecoveryMode, + } = fixture; + + const stEthBalanceBefore = await balancerSfrxWstRETHStrategy[ + "checkBalance(address)" + ](stETH.address); + const rethBalanceBefore = await balancerSfrxWstRETHStrategy[ + "checkBalance(address)" + ](reth.address); + const frxEthBalanceBefore = await balancerSfrxWstRETHStrategy[ + "checkBalance(address)" + ](frxETH.address); + + const oethVaultSigner = await impersonateAndFund(oethVault.address); + + await expect( + balancerSfrxWstRETHStrategy + .connect(oethVaultSigner) + .recoveryModeWithdrawAll() + ).to.be.revertedWith("Pool not in recovery mode"); + + await enableRecoveryMode(); + + await balancerSfrxWstRETHStrategy + .connect(oethVaultSigner) + .recoveryModeWithdrawAll(); + + const stEthBalanceDiff = stEthBalanceBefore.sub( + await balancerSfrxWstRETHStrategy["checkBalance(address)"]( + stETH.address + ) + ); + const rethBalanceDiff = rethBalanceBefore.sub( + await balancerSfrxWstRETHStrategy["checkBalance(address)"](reth.address) + ); + const frxEthBalanceDiff = frxEthBalanceBefore.sub( + await balancerSfrxWstRETHStrategy["checkBalance(address)"]( + frxETH.address + ) + ); + + expect(stEthBalanceDiff).to.be.gte(await units("15", stETH), 1); + expect(rethBalanceDiff).to.be.gte(await units("15", reth), 1); + expect(frxEthBalanceDiff).to.be.gte(await units("15", frxETH), 1); + }); }); describe("Large withdraw", function () { diff --git a/contracts/test/strategies/balancerMetaStablePool.fork-test.js b/contracts/test/strategies/balancerMetaStablePool.fork-test.js index ab665ed067..2712d4c25a 100644 --- a/contracts/test/strategies/balancerMetaStablePool.fork-test.js +++ b/contracts/test/strategies/balancerMetaStablePool.fork-test.js @@ -369,6 +369,16 @@ describe("ForkTest: Balancer MetaStablePool rETH/WETH Strategy", function () { expect(stEthBalanceDiff).to.be.gte(await units("15", reth), 1); }); + it("Should fail withdrawing all of pool liquidity in recovery mode (pool doesn't support it)", async function () { + const { oethVault, balancerREthStrategy } = fixture; + + const oethVaultSigner = await impersonateAndFund(oethVault.address); + + await expect( + balancerREthStrategy.connect(oethVaultSigner).recoveryModeWithdrawAll() + ).to.be.reverted; + }); + it("Should be able to withdraw with higher withdrawal deviation", async function () {}); });