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

M-01-late Balancer Review: more robust withdraw from strategies #1928

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/IVault.sol
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ interface IVault {

function withdrawAllFromStrategies() external;

function faultTolerantWithdrawAllFromStrategies() external;

function withdrawFromStrategy(
address _strategyFromAddress,
address[] calldata _assets,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/**
* @title OETH Balancer BalancerComposablePoolTest Strategy
* This one is required to expose internal functions to our test suite. In fixtures
* we replace the mainnet byte code with the one from this contract.
*
* @author Origin Protocol Inc
*/
import { BalancerComposablePoolStrategy } from "../strategies/balancer/BalancerComposablePoolStrategy.sol";

contract BalancerComposablePoolBrokenTestStrategy is
BalancerComposablePoolStrategy
{
constructor(
BaseStrategyConfig memory _stratConfig,
BaseBalancerConfig memory _balancerConfig,
address _auraRewardPoolAddress,
uint256 _bptTokenPoolPosition
)
BalancerComposablePoolStrategy(
_stratConfig,
_balancerConfig,
_auraRewardPoolAddress,
_bptTokenPoolPosition
)
{}

function withdrawAll() external override onlyVaultOrGovernor nonReentrant {
require(false, "Simulating a failed call");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ contract BalancerMetaPoolStrategy is BaseAuraStrategy {
/* This check is triggered when the _deposit is called with
* a duplicate asset in the _strategyAssets array
*
* A duplicate asset supplied with 0 amount or an amount close to
* A duplicate asset supplied with 0 amount or an amount close to
* 0 that wraps to a 0 amount will still pass this check.
*/
require(
Expand Down Expand Up @@ -441,7 +441,13 @@ contract BalancerMetaPoolStrategy is BaseAuraStrategy {
*
* Is only executable by the OToken's Vault or the Governor.
*/
function withdrawAll() external override onlyVaultOrGovernor nonReentrant {
function withdrawAll()
external
virtual
override
onlyVaultOrGovernor
nonReentrant
{
_withdrawAll(false);
}

Expand Down
16 changes: 16 additions & 0 deletions contracts/contracts/vault/VaultAdmin.sol
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,22 @@ contract VaultAdmin is VaultStorage {
}
}

/**
* @notice Withdraws all assets from all the strategies and sends assets to the Vault.
* and doesn't revert event when `withdrawAll` of a strategy reverts.
*/
function faultTolerantWithdrawAllFromStrategies()
external
onlyGovernorOrStrategist
{
uint256 stratCount = allStrategies.length;
for (uint256 i = 0; i < stratCount; ++i) {
try IStrategy(allStrategies[i]).withdrawAll() {} catch {
emit WithdrawFromStrategyFailed(allStrategies[i]);
}
}
}

/***************************************
Utils
****************************************/
Expand Down
1 change: 1 addition & 0 deletions contracts/contracts/vault/VaultStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ contract VaultStorage is Initializable, Governable {
uint256 _fromAssetAmount,
uint256 _toAssetAmount
);
event WithdrawFromStrategyFailed(address _strategyAddress);

// Assets supported by the Vault, i.e. Stablecoins
enum UnitConversion {
Expand Down
7 changes: 7 additions & 0 deletions contracts/test/abi/IVault.json
Original file line number Diff line number Diff line change
Expand Up @@ -991,6 +991,13 @@
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "faultTolerantWithdrawAllFromStrategies",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
Expand Down
36 changes: 31 additions & 5 deletions contracts/test/fixture/_fixture.js
Original file line number Diff line number Diff line change
Expand Up @@ -320,12 +320,12 @@ const defaultFixture = deployments.createFixture(async () => {
fraxEthStrategyProxy.address
);

const balancerRethStrategyProxy = await ethers.getContract(
const balancerREthStrategyProxy = await ethers.getContract(
"OETHBalancerMetaPoolrEthStrategyProxy"
);
balancerREthStrategy = await ethers.getContractAt(
"BalancerMetaPoolStrategy",
balancerRethStrategyProxy.address
balancerREthStrategyProxy.address
);

const balancerSfrxWstRETHStrategyProxy = await ethers.getContract(
Expand Down Expand Up @@ -905,6 +905,7 @@ async function convexVaultFixture() {
*/
async function balancerREthFixture(config = { defaultStrategy: true }) {
const fixture = await defaultFixture();

await hotDeployOption(fixture, "balancerREthFixture", {
isOethFixture: true,
});
Expand Down Expand Up @@ -1035,8 +1036,9 @@ async function balancerFrxETHwstETHeETHFixture(
*/
async function balancerRethWETHExposeFunctionFixture() {
const fixture = await balancerREthFixture();
await hotDeployOption(fixture, "balancerRethWETHExposeFunctionFixture");

await hotDeployOption(fixture, "balancerRethWETHExposeFunctionFixture", {
forceDeployStrategy: true,
});
return fixture;
}

Expand All @@ -1055,7 +1057,30 @@ async function balancerSfrxETHRETHWstETHExposeFunctionFixture() {
const fixture = await balancerFrxETHwstETHeETHFixture();
await hotDeployOption(
fixture,
"balancerSfrxETHRETHWstETHExposeFunctionFixture"
"balancerSfrxETHRETHWstETHExposeFunctionFixture",
{
forceDeployStrategy: true,
}
);
return fixture;
}

/**
* Configure a Vault with the Balancer strategy for frxEth/Reth/wstEth pool and
* replace the byte code with the one that fails on a withdrawAll call
*/
async function balancerSfrxETHRETHWstETHBrokenWithdrawalFixture() {
const fixture = await balancerFrxETHwstETHeETHFixture();
await hotDeployOption(
fixture,
"balancerSfrxETHRETHWstETHBrokenWithdrawalFixture",
{
isOethFixture: true,
/* force deploy strategy (+ vaultAdmin in this case) for test suite
* to be able to run a VaultAdmin test that hasn't been deployed yet.
*/
forceDeployStrategy: true,
}
);
return fixture;
}
Expand Down Expand Up @@ -2083,6 +2108,7 @@ module.exports = {
balancerRethWETHExposeFunctionFixture,
balancerSfrxETHRETHWstETHExposeFunctionFixture,
balancerSfrxETHRETHWstETHMissConfiguredStrategy,
balancerSfrxETHRETHWstETHBrokenWithdrawalFixture,
fluxStrategyFixture,
buybackFixture,
nodeSnapshot,
Expand Down
86 changes: 70 additions & 16 deletions contracts/test/fixture/_hot-deploy.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,12 @@ async function constructNewContract(fixture, implContractName) {
],
addresses.mainnet.rETH_WETH_AuraRewards, // Address of the Aura rewards contract
];
} else if (implContractName === "BalancerComposablePoolTestStrategy") {
} else if (
[
"BalancerComposablePoolBrokenTestStrategy",
"BalancerComposablePoolTestStrategy",
].includes(implContractName)
) {
return [
[
addresses.mainnet.wstETH_sfrxETH_rETH_BPT,
Expand Down Expand Up @@ -101,22 +106,44 @@ async function constructNewContract(fixture, implContractName) {
async function hotDeployOption(
fixture,
fixtureName,
config = { isOethFixture: false }
config = { isOethFixture: false, forceDeployStrategy: false }
) {
if (!isFork) return;

const hotDeployOptions = (process.env.HOT_DEPLOY || "")
.split(",")
.map((item) => item.trim());
const { isOethFixture } = config;
const deployStrat = hotDeployOptions.includes("strategy");
const { isOethFixture, forceDeployStrategy } = config;
const deployStrat =
hotDeployOptions.includes("strategy") || forceDeployStrategy;
const deployVaultCore = hotDeployOptions.includes("vaultCore");
const deployVaultAdmin = hotDeployOptions.includes("vaultAdmin");
const deployHarvester = hotDeployOptions.includes("harvester");

log(`Running fixture hot deployment w/ config; isOethFixture:${isOethFixture} strategy:${!!deployStrat}
log(`Running fixture [${fixtureName}] hot deployment w/ config; isOethFixture:${isOethFixture} strategy:${!!deployStrat}
vaultCore:${!!deployVaultCore} vaultAdmin:${!!deployVaultAdmin} harvester:${!!deployHarvester}`);

const cacheAssetsAndProviders = async (strategyVarName) => {
const proxy = await ethers.getContractAt(
"InitializeGovernedUpgradeabilityProxy",
fixture[strategyVarName].address
);

const implAddress = await proxy.implementation();
log(`Current impl address: ${implAddress}`);
// new version with cached rate providers / assets has not been deployed yet
if (implAddress === "0xAaA1d497fdff9a88048743Db31d3173a2E442A3D") {
log(`Caching rate providers and pool assets`);
await fixture[strategyVarName].connect(fixture.josh).cachePoolAssets();

await fixture[strategyVarName].connect(fixture.josh).cacheRateProviders();
} else {
throw new Error(
`New ${strategyVarName} deployed, removed manual caching!`
);
}
};

if (deployStrat) {
if (fixtureName === "balancerREthFixture") {
await hotDeployFixture(
Expand All @@ -125,14 +152,7 @@ async function hotDeployOption(
"BalancerMetaPoolStrategy" // implContractName
);

// IMPORTANT: remove once rETH/WETH is redeployed with the new code base
await fixture.balancerREthStrategy
.connect(fixture.josh)
.cachePoolAssets();
// IMPORTANT also remove this one
await fixture.balancerREthStrategy
.connect(fixture.josh)
.cacheRateProviders();
await cacheAssetsAndProviders("balancerREthStrategy");
} else if (fixtureName === "morphoCompoundFixture") {
await hotDeployFixture(
fixture, // fixture
Expand All @@ -157,16 +177,50 @@ async function hotDeployOption(
"balancerREthStrategy", // fixtureStrategyVarName
"BalancerMetaPoolTestStrategy" // implContractName
);

await cacheAssetsAndProviders("balancerREthStrategy");
} else if (
fixtureName === "balancerSfrxETHRETHWstETHExposeFunctionFixture"
fixtureName === "balancerSfrxETHRETHWstETHBrokenWithdrawalFixture"
) {
await hotDeployFixture(
fixture, // fixture
"balancerSfrxWstRETHStrategy", // fixtureStrategyVarName
"BalancerComposablePoolTestStrategy" // implContractName
"BalancerComposablePoolBrokenTestStrategy" // implContractName
);

/*
* Delete this piece of code once the new VaultAdmin implementation is deployed.
*/
const oethVaultAdminImplAddress =
"0x" +
(
await ethers.provider.send("eth_getStorageAt", [
fixture.oethVault.address,
"0xa2bd3d3cf188a41358c8b401076eb59066b09dec5775650c0de4c55187d17bd9", // Vault admin implementation position
"latest", // block
])
).substring(26);

if (
oethVaultAdminImplAddress !==
"0x31a91336414d3b955e494e7d485a6b06b55fc8fb"
) {
throw Error(
"OETHVaultAdmin has been re-deployed. Hot-deploy shouldn't be re-deploying it anymore."
);
}

await hotDeployVaultAdmin(
fixture,
true, // deploy VaultAdmin
false, // deploy VaultCore
true // isOethFixture
);
} else if (
fixtureName === "deployBalancerFrxEethRethWstEThStrategyMissConfigured"
[
"balancerSfrxETHRETHWstETHExposeFunctionFixture",
"deployBalancerFrxEethRethWstEThStrategyMissConfigured",
].includes(fixtureName)
) {
await hotDeployFixture(
fixture, // fixture
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const {
createFixtureLoader,
balancerSfrxETHRETHWstETHExposeFunctionFixture,
balancerSfrxETHRETHWstETHMissConfiguredStrategy,
balancerSfrxETHRETHWstETHBrokenWithdrawalFixture,
} = require("../fixture/_fixture");

const { tiltPool, unTiltPool } = require("../fixture/_pool_tilt");
Expand Down Expand Up @@ -433,6 +434,37 @@ describe("ForkTest: Balancer ComposableStablePool sfrxETH/wstETH/rETH Strategy",
});
}

it("Should be able to call faultTolerantWithdrawAllFromStrategies even when one errors out", async function () {
const fixture = await balancerSfrxETHRETHWstETHBrokenWithdrawalFixture();
const { timelock, oethVault, balancerSfrxWstRETHStrategy } = fixture;
const strategyAddress = balancerSfrxWstRETHStrategy.address.toLowerCase();

// Withdraw all from strategies
const tx = await oethVault
.connect(timelock)
.faultTolerantWithdrawAllFromStrategies();
const events = (await tx.wait()).events || [];
/*
* If OethVaultAdmin has triggered the "WithdrawFromStrategyFailed" with the
* balancerSfrxWstRETHStrategy address the below variable will equal that address,
* otherwise it will be `undefined`
*/
const balancerStrategyAddressOption = events
// 0xbf15... -> "WithdrawFromStrategyFailed" event
.filter(
(e) =>
e.topics[0] ===
"0xbf15aee350c3b4fb830dccbe5255de03252ad7eea9c1e5e721c2280704ef5b37"
)
.map((e) => "0x" + e.data.substring(26).toLowerCase())
.find((evtStrategyAddress) => evtStrategyAddress === strategyAddress);

expect(balancerStrategyAddressOption).to.not.be.undefined;

await expect(oethVault.connect(timelock).withdrawAllFromStrategies()).to
.be.reverted;
});

it("Should be able to withdraw all of pool liquidity", async function () {
const { oethVault, stETH, frxETH, reth, balancerSfrxWstRETHStrategy } =
fixture;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,11 @@ describe("ForkTest: Balancer MetaStablePool rETH/WETH Strategy", function () {
});

it(`Should fail when duplicating an asset in withdrawal call`, async function () {
/* using expose function so that the code is force-deployed - unit needed,
* that is until we deploy updated implementation to the mainnet
*/
fixture = await balancerRethWETHExposeFunctionFixture();

const { balancerREthStrategy, oethVault, weth } = fixture;

const oethVaultSigner = await impersonateAndFund(oethVault.address);
Expand Down
Loading