Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

L-02 Balancer Pool Recovery Mode Is Not Supported #1923

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions contracts/contracts/interfaces/balancer/IBalancerPool.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ interface IBalancerPool {
external
view
returns (IRateProvider[] memory providers);

function inRecoveryMode() external view returns (bool);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,18 @@ 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";
import "@openzeppelin/contracts/utils/math/Math.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:
*
Expand Down Expand Up @@ -439,6 +444,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();
Expand All @@ -459,12 +476,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);

Expand Down
13 changes: 13 additions & 0 deletions contracts/test/fixture/_fixture.js
Original file line number Diff line number Diff line change
Expand Up @@ -1025,6 +1025,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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,59 @@ describe("ForkTest: Balancer ComposableStablePool sfrxETH/wstETH/rETH Strategy",
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);
});

it("Should be able to withdraw all of pool's liquidity twice", async function () {
const { oethVault, balancerSfrxWstRETHStrategy } = fixture;

Expand Down
10 changes: 10 additions & 0 deletions contracts/test/strategies/balancerMetaStablePool.fork-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,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 () {});
});

Expand Down
Loading