diff --git a/packages/tasks/contracts/interfaces/liquidity/balancer/IBalancerBPTExiter.sol b/packages/tasks/contracts/interfaces/liquidity/balancer/IBalancerBPTExiter.sol
new file mode 100644
index 00000000..9b948525
--- /dev/null
+++ b/packages/tasks/contracts/interfaces/liquidity/balancer/IBalancerBPTExiter.sol
@@ -0,0 +1,52 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+pragma solidity >=0.8.0;
+
+import '../../ITask.sol';
+
+/**
+ * @dev BPT exiter task interface
+ */
+interface IBalancerBPTExiter is ITask {
+ /**
+ * @dev The token is zero
+ */
+ error TaskTokenZero();
+
+ /**
+ * @dev The amount is zero
+ */
+ error TaskAmountZero();
+
+ /**
+ * @dev The post balance is lower than the pre balance
+ */
+ error TaskPostBalanceUnexpected(uint256 postBalance, uint256 preBalance);
+
+ /**
+ * @dev The amount out is lower than the minimum amount out
+ */
+ error TaskBadAmountOut(uint256 amountOut, uint256 minAmountOut);
+
+ /**
+ * @dev Tells the address of the Balancer vault. It cannot be changed.
+ */
+ function balancerVault() external returns (address);
+
+ /**
+ * @dev Execute Balancer BPT exiter
+ */
+ function call(address token, uint256 amount) external;
+}
diff --git a/packages/tasks/contracts/interfaces/liquidity/balancer/IBalancerBoostedPool.sol b/packages/tasks/contracts/interfaces/liquidity/balancer/IBalancerBoostedPool.sol
new file mode 100644
index 00000000..f46c9a9c
--- /dev/null
+++ b/packages/tasks/contracts/interfaces/liquidity/balancer/IBalancerBoostedPool.sol
@@ -0,0 +1,23 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+pragma solidity ^0.8.0;
+
+import './IBalancerPool.sol';
+
+interface IBalancerBoostedPool is IBalancerPool {
+ function getRate() external view returns (uint256);
+
+ function getBptIndex() external view returns (uint256);
+}
diff --git a/packages/tasks/contracts/interfaces/liquidity/balancer/IBalancerLinearPool.sol b/packages/tasks/contracts/interfaces/liquidity/balancer/IBalancerLinearPool.sol
new file mode 100644
index 00000000..67555d6f
--- /dev/null
+++ b/packages/tasks/contracts/interfaces/liquidity/balancer/IBalancerLinearPool.sol
@@ -0,0 +1,23 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+pragma solidity ^0.8.0;
+
+import './IBalancerPool.sol';
+
+interface IBalancerLinearPool is IBalancerPool {
+ function getRate() external view returns (uint256);
+
+ function getMainToken() external view returns (address);
+}
diff --git a/packages/tasks/contracts/interfaces/liquidity/balancer/IBalancerPool.sol b/packages/tasks/contracts/interfaces/liquidity/balancer/IBalancerPool.sol
new file mode 100644
index 00000000..7afc6cb5
--- /dev/null
+++ b/packages/tasks/contracts/interfaces/liquidity/balancer/IBalancerPool.sol
@@ -0,0 +1,21 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+pragma solidity ^0.8.0;
+
+import '@openzeppelin/contracts/token/ERC20/IERC20.sol';
+
+interface IBalancerPool is IERC20 {
+ function getPoolId() external view returns (bytes32);
+}
diff --git a/packages/tasks/contracts/interfaces/liquidity/balancer/IBalancerVault.sol b/packages/tasks/contracts/interfaces/liquidity/balancer/IBalancerVault.sol
new file mode 100644
index 00000000..f9bafa73
--- /dev/null
+++ b/packages/tasks/contracts/interfaces/liquidity/balancer/IBalancerVault.sol
@@ -0,0 +1,90 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+pragma solidity ^0.8.0;
+
+import '@openzeppelin/contracts/token/ERC20/IERC20.sol';
+
+interface IBalancerVault {
+ function getPool(bytes32 poolId) external view returns (address, uint256);
+
+ function getPoolTokens(bytes32 poolId)
+ external
+ view
+ returns (IERC20[] memory tokens, uint256[] memory balances, uint256 lastChangeBlock);
+
+ struct JoinPoolRequest {
+ IERC20[] assets;
+ uint256[] maxAmountsIn;
+ bytes userData;
+ bool fromInternalBalance;
+ }
+
+ function joinPool(bytes32 poolId, address sender, address recipient, JoinPoolRequest memory request)
+ external
+ payable;
+
+ struct ExitPoolRequest {
+ IERC20[] assets;
+ uint256[] minAmountsOut;
+ bytes userData;
+ bool toInternalBalance;
+ }
+
+ function exitPool(bytes32 poolId, address sender, address payable recipient, ExitPoolRequest memory request)
+ external;
+
+ enum SwapKind {
+ GIVEN_IN,
+ GIVEN_OUT
+ }
+
+ struct SingleSwap {
+ bytes32 poolId;
+ SwapKind kind;
+ address assetIn;
+ address assetOut;
+ uint256 amount;
+ bytes userData;
+ }
+
+ struct FundManagement {
+ address sender;
+ bool fromInternalBalance;
+ address payable recipient;
+ bool toInternalBalance;
+ }
+
+ function swap(SingleSwap memory singleSwap, FundManagement memory funds, uint256 limit, uint256 deadline)
+ external
+ payable
+ returns (uint256);
+
+ struct BatchSwapStep {
+ bytes32 poolId;
+ uint256 assetInIndex;
+ uint256 assetOutIndex;
+ uint256 amount;
+ bytes userData;
+ }
+
+ function batchSwap(
+ SwapKind kind,
+ BatchSwapStep[] memory swaps,
+ address[] memory assets,
+ FundManagement memory funds,
+ int256[] memory limits,
+ uint256 deadline
+ ) external payable returns (int256[] memory);
+}
diff --git a/packages/tasks/contracts/liquidity/balancer/BalancerBPTExiter.sol b/packages/tasks/contracts/liquidity/balancer/BalancerBPTExiter.sol
new file mode 100644
index 00000000..d9796f27
--- /dev/null
+++ b/packages/tasks/contracts/liquidity/balancer/BalancerBPTExiter.sol
@@ -0,0 +1,296 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+pragma solidity ^0.8.0;
+
+import '@openzeppelin/contracts/token/ERC20/IERC20.sol';
+import '@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol';
+
+import '../../Task.sol';
+import '../../interfaces/liquidity/balancer/IBalancerBPTExiter.sol';
+import '../../interfaces/liquidity/balancer/IBalancerLinearPool.sol';
+import '../../interfaces/liquidity/balancer/IBalancerBoostedPool.sol';
+import '../../interfaces/liquidity/balancer/IBalancerPool.sol';
+import '../../interfaces/liquidity/balancer/IBalancerVault.sol';
+
+// solhint-disable avoid-low-level-calls
+
+/**
+ * @title Balancer BPT exiter
+ * @dev Task that offers the components to exit Balancer pools
+ */
+contract BalancerBPTExiter is IBalancerBPTExiter, Task {
+ // Private constant used to exit Balancer pools
+ uint256 private constant EXACT_BPT_IN_FOR_TOKENS_OUT = 1;
+
+ // Execution type for relayers
+ bytes32 public constant override EXECUTION_TYPE = keccak256('BPT_EXITER');
+
+ // Balancer vault reference. It cannot be changed.
+ address public override balancerVault;
+
+ /**
+ * @dev Balancer BPT exit config. Only used in the initializer.
+ */
+ struct BPTExitConfig {
+ address balancerVault;
+ TaskConfig taskConfig;
+ }
+
+ /**
+ * @dev Initializes a Balancer BPT exiter
+ * @param config Balancer BPT exit config
+ */
+ function initialize(BPTExitConfig memory config) external virtual initializer {
+ __BalancerBPTExiter_init(config);
+ }
+
+ /**
+ * @dev Initializes the Balancer BPT exiter. It does call upper contracts initializers.
+ * @param config Balancer BPT exit config
+ */
+ function __BalancerBPTExiter_init(BPTExitConfig memory config) internal onlyInitializing {
+ __Task_init(config.taskConfig);
+ __BalancerBPTExiter_init_unchained(config);
+ }
+
+ /**
+ * @dev Initializes the Balancer BPT exiter. It does not call upper contracts initializers.
+ * @param config Balancer BPT exit config
+ */
+ function __BalancerBPTExiter_init_unchained(BPTExitConfig memory config) internal onlyInitializing {
+ balancerVault = config.balancerVault;
+ }
+
+ /**
+ * @dev Execute Balancer BPT exiter
+ * @param token Address of the Balancer pool token to exit
+ * @param amount Amount of Balancer pool tokens to exit
+ */
+ function call(address token, uint256 amount) external override authP(authParams(token, amount)) {
+ if (amount == 0) amount = getTaskAmount(token);
+ _beforeBalancerBPTExiter(token, amount);
+
+ (bytes memory data, IERC20[] memory tokensOut, uint256[] memory minAmountsOut) = _buildSwapCall(token, amount);
+ uint256[] memory preBalances = _getBalances(tokensOut);
+
+ ISmartVault(smartVault).call(token, abi.encodeWithSelector(IERC20.approve.selector, balancerVault, amount), 0);
+ ISmartVault(smartVault).call(balancerVault, data, 0);
+
+ uint256[] memory amountsOut = _getAmountsOut(tokensOut, preBalances, minAmountsOut);
+ _afterBalancerBPTExiter(token, amount, tokensOut, amountsOut);
+ }
+
+ /**
+ * @dev Before Balancer BPT exiter hook
+ */
+ function _beforeBalancerBPTExiter(address token, uint256 amount) internal virtual {
+ _beforeTask(token, amount);
+ if (token == address(0)) revert TaskTokenZero();
+ if (amount == 0) revert TaskAmountZero();
+ }
+
+ /**
+ * @dev After Balancer BPT exiter hook
+ */
+ function _afterBalancerBPTExiter(
+ address tokenIn,
+ uint256 amountIn,
+ IERC20[] memory tokensOut,
+ uint256[] memory amountsOut
+ ) internal virtual {
+ for (uint256 i = 0; i < tokensOut.length; i++) _increaseBalanceConnector(address(tokensOut[i]), amountsOut[i]);
+ _afterTask(tokenIn, amountIn);
+ }
+
+ /**
+ * @dev Builds the corresponding data to swap a BPT into its underlying tokens
+ * @param pool Address of the Balancer pool token to swap
+ * @param amount Amount of Balancer pool tokens to swap
+ */
+ function _buildSwapCall(address pool, uint256 amount)
+ private
+ view
+ returns (bytes memory data, IERC20[] memory tokensOut, uint256[] memory minAmountsOut)
+ {
+ try IBalancerLinearPool(pool).getMainToken() returns (address main) {
+ return _buildLinearPoolSwap(pool, amount, main);
+ } catch {
+ try IBalancerBoostedPool(pool).getBptIndex() returns (uint256 bptIndex) {
+ return _buildBoostedPoolSwap(pool, amount, bptIndex);
+ } catch {
+ return _buildNormalPoolExit(pool, amount);
+ }
+ }
+ }
+
+ /**
+ * @dev Exit normal pools using an exact BPT for tokens out. Note that there is no need to compute
+ * minimum amounts since this is considered a proportional exit.
+ * @param pool Address of the Balancer pool token to exit
+ * @param amount Amount of Balancer pool tokens to exit
+ */
+ function _buildNormalPoolExit(address pool, uint256 amount)
+ private
+ view
+ returns (bytes memory data, IERC20[] memory tokens, uint256[] memory minAmountsOut)
+ {
+ // Fetch the list of tokens of the pool
+ bytes32 poolId = IBalancerPool(pool).getPoolId();
+ (tokens, , ) = IBalancerVault(balancerVault).getPoolTokens(poolId);
+
+ // Proportional exit
+ minAmountsOut = new uint256[](tokens.length);
+ IBalancerVault.ExitPoolRequest memory request = IBalancerVault.ExitPoolRequest({
+ assets: tokens,
+ minAmountsOut: minAmountsOut,
+ userData: abi.encodePacked(EXACT_BPT_IN_FOR_TOKENS_OUT, amount),
+ toInternalBalance: false
+ });
+
+ data = abi.encodeWithSelector(
+ IBalancerVault.exitPool.selector,
+ poolId,
+ address(smartVault),
+ payable(address(smartVault)),
+ request
+ );
+ }
+
+ /**
+ * @dev Exit linear pools using a swap request in exchange for the main token of the pool. The min amount out is
+ * computed based on the current rate of the linear pool.
+ * @param pool Address of the Balancer pool token to swap
+ * @param amount Amount of Balancer pool tokens to swap
+ * @param main Address of the main token
+ */
+ function _buildLinearPoolSwap(address pool, uint256 amount, address main)
+ private
+ view
+ returns (bytes memory data, IERC20[] memory tokensOut, uint256[] memory minAmountsOut)
+ {
+ // Compute minimum amount out in the main token
+ uint256 rate = IBalancerLinearPool(pool).getRate();
+ uint256 decimals = IERC20Metadata(main).decimals();
+ uint256 minAmountOut = _getMinAmountOut(rate, decimals);
+
+ // Swap from linear to main token
+ IBalancerVault.SingleSwap memory request = IBalancerVault.SingleSwap({
+ poolId: IBalancerPool(pool).getPoolId(),
+ kind: IBalancerVault.SwapKind.GIVEN_IN,
+ assetIn: pool,
+ assetOut: main,
+ amount: amount,
+ userData: new bytes(0)
+ });
+
+ // Build fund management object: smart vault is the sender and recipient
+ IBalancerVault.FundManagement memory funds = IBalancerVault.FundManagement({
+ sender: address(smartVault),
+ fromInternalBalance: false,
+ recipient: payable(address(smartVault)),
+ toInternalBalance: false
+ });
+
+ data = abi.encodeWithSelector(IBalancerVault.swap.selector, request, funds, minAmountOut, block.timestamp);
+ tokensOut = new IERC20[](1);
+ tokensOut[0] = IERC20(main);
+ minAmountsOut = new uint256[](1);
+ minAmountsOut[0] = minAmountOut;
+ }
+
+ /**
+ * @dev Exit boosted pools using a swap request in exchange for the first underlying token of the pool. The min
+ * amount out is computed based on the current rate of the boosted pool.
+ * @param pool Address of the Balancer pool token to swap
+ * @param amount Amount of Balancer pool tokens to swap
+ * @param bptIndex Index of the BPT in the list of tokens tracked by the Balancer Vault
+ */
+ function _buildBoostedPoolSwap(address pool, uint256 amount, uint256 bptIndex)
+ private
+ view
+ returns (bytes memory data, IERC20[] memory tokensOut, uint256[] memory minAmountsOut)
+ {
+ // Pick the first underlying token of the boosted pool
+ bytes32 poolId = IBalancerPool(pool).getPoolId();
+ (IERC20[] memory tokens, , ) = IBalancerVault(balancerVault).getPoolTokens(poolId);
+ address underlying = address(bptIndex == 0 ? tokens[1] : tokens[0]);
+
+ // Compute minimum amount out in the underlying token
+ uint256 rate = IBalancerBoostedPool(pool).getRate();
+ uint256 decimals = IERC20Metadata(underlying).decimals();
+ uint256 minAmountOut = _getMinAmountOut(rate, decimals);
+
+ // Swap from BPT to underlying token
+ IBalancerVault.SingleSwap memory request = IBalancerVault.SingleSwap({
+ poolId: IBalancerPool(pool).getPoolId(),
+ kind: IBalancerVault.SwapKind.GIVEN_IN,
+ assetIn: pool,
+ assetOut: underlying,
+ amount: amount,
+ userData: new bytes(0)
+ });
+
+ // Build fund management object: smart vault is the sender and recipient
+ IBalancerVault.FundManagement memory funds = IBalancerVault.FundManagement({
+ sender: address(smartVault),
+ fromInternalBalance: false,
+ recipient: payable(address(smartVault)),
+ toInternalBalance: false
+ });
+
+ data = abi.encodeWithSelector(IBalancerVault.swap.selector, request, funds, minAmountOut, block.timestamp);
+ tokensOut = new IERC20[](1);
+ tokensOut[0] = IERC20(underlying);
+ minAmountsOut = new uint256[](1);
+ minAmountsOut[0] = minAmountOut;
+ }
+
+ /**
+ * @dev Tells the min amount out of a swap based on the current rate and decimals of the token
+ * @param rate Current rate of the pool
+ * @param decimals Decimals of the token
+ */
+ function _getMinAmountOut(uint256 rate, uint256 decimals) private pure returns (uint256) {
+ return decimals <= 18 ? (rate / (10**(18 - decimals))) : (rate * (10**(decimals - 18)));
+ }
+
+ /**
+ * @dev Tells the balances of a list of tokens
+ */
+ function _getBalances(IERC20[] memory tokens) private view returns (uint256[] memory balances) {
+ balances = new uint256[](tokens.length);
+ for (uint256 i = 0; i < tokens.length; i++) balances[i] = tokens[i].balanceOf(smartVault);
+ }
+
+ /**
+ * @dev Tells the amounts out of a list of tokens and previous balances, and checks that they are above the
+ * minimum amounts out
+ */
+ function _getAmountsOut(IERC20[] memory tokens, uint256[] memory preBalances, uint256[] memory minAmountsOut)
+ private
+ view
+ returns (uint256[] memory amountsOut)
+ {
+ amountsOut = new uint256[](tokens.length);
+ for (uint256 i = 0; i < tokens.length; i++) {
+ uint256 preBalance = preBalances[i];
+ uint256 postBalance = tokens[i].balanceOf(smartVault);
+ if (postBalance < preBalance) revert TaskPostBalanceUnexpected(postBalance, preBalance);
+ uint256 amountOut = postBalance - preBalance;
+ if (amountOut < minAmountsOut[i]) revert TaskBadAmountOut(amountOut, minAmountsOut[i]);
+ amountsOut[i] = amountOut;
+ }
+ }
+}
diff --git a/packages/tasks/package.json b/packages/tasks/package.json
index f46dd9cb..566dfdd3 100644
--- a/packages/tasks/package.json
+++ b/packages/tasks/package.json
@@ -18,6 +18,7 @@
"lint:solidity": "solhint 'contracts/**/*.sol' --config ../../node_modules/solhint-config-mimic/index.js",
"lint:typescript": "eslint . --ext .ts",
"test": "hardhat test",
+ "test:mainnet": "yarn test --fork mainnet --block-number 17525323 --chain-id 1",
"prepare": "yarn build"
},
"dependencies": {
diff --git a/packages/tasks/test/liquidity/balancer/BalancerBPTExiter.mainnet.ts b/packages/tasks/test/liquidity/balancer/BalancerBPTExiter.mainnet.ts
new file mode 100644
index 00000000..1419802f
--- /dev/null
+++ b/packages/tasks/test/liquidity/balancer/BalancerBPTExiter.mainnet.ts
@@ -0,0 +1,304 @@
+import {
+ assertIndirectEvent,
+ deployProxy,
+ fp,
+ getSigners,
+ impersonate,
+ instanceAt,
+ toUSDC,
+ ZERO_ADDRESS,
+ ZERO_BYTES32,
+} from '@mimic-fi/v3-helpers'
+import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address'
+import { expect } from 'chai'
+import { BigNumber, Contract } from 'ethers'
+
+import { buildEmptyTaskConfig, deployEnvironment } from '../../../dist'
+
+/* eslint-disable no-secrets/no-secrets */
+
+const USDC = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'
+const BALANCER_VAULT = '0xBA12222222228d8Ba445958a75a0704d566BF2C8'
+
+describe('BalancerBPTExiter', function () {
+ let task: Contract
+ let smartVault: Contract, authorizer: Contract, owner: SignerWithAddress
+
+ before('setup', async () => {
+ // eslint-disable-next-line prettier/prettier
+ ([, owner] = await getSigners())
+ ;({ authorizer, smartVault } = await deployEnvironment(owner))
+ })
+
+ beforeEach('deploy task', async () => {
+ task = await deployProxy(
+ 'BalancerBPTExiter',
+ [],
+ [
+ {
+ balancerVault: BALANCER_VAULT,
+ taskConfig: buildEmptyTaskConfig(owner, smartVault),
+ },
+ ]
+ )
+ })
+
+ describe('initialize', () => {
+ it('has a reference to the balancer vault', async () => {
+ expect(await task.balancerVault()).to.be.equal(BALANCER_VAULT)
+ })
+ })
+
+ describe('call', () => {
+ beforeEach('authorize task', async () => {
+ const callRole = smartVault.interface.getSighash('call')
+ await authorizer.connect(owner).authorize(task.address, smartVault.address, callRole, [])
+ })
+
+ context('when the sender is authorized', () => {
+ beforeEach('set sender', async () => {
+ const callRole = task.interface.getSighash('call')
+ await authorizer.connect(owner).authorize(owner.address, task.address, callRole, [])
+ task = task.connect(owner)
+ })
+
+ context('when the token is not zero', () => {
+ const amount = fp(5)
+ let pool: Contract, usdc: Contract, balancer: Contract
+
+ beforeEach('load contracts', async () => {
+ usdc = await instanceAt('IERC20', USDC)
+ balancer = await instanceAt('IBalancerVault', BALANCER_VAULT)
+ })
+
+ const setUpPool = (poolContractName: string, poolAddress: string, whaleAddress: string) => {
+ beforeEach('load pool', async () => {
+ pool = await instanceAt(poolContractName, poolAddress)
+ const whale = await impersonate(whaleAddress, fp(10))
+ await pool.connect(whale).transfer(smartVault.address, amount)
+ })
+ }
+
+ context('when the amount is not zero', () => {
+ beforeEach('fund smart vault', async () => {
+ const whale = await impersonate('0xDa9CE944a37d218c3302F6B82a094844C6ECEb17', fp(10))
+ await usdc.connect(whale).transfer(smartVault.address, toUSDC(1000))
+ })
+
+ context('when the threshold has passed', () => {
+ const threshold = amount
+
+ const setTokenThreshold = () => {
+ beforeEach('set default token threshold', async () => {
+ const setDefaultTokenThresholdRole = task.interface.getSighash('setDefaultTokenThreshold')
+ await authorizer.connect(owner).authorize(owner.address, task.address, setDefaultTokenThresholdRole, [])
+ await task.connect(owner).setDefaultTokenThreshold(pool.address, threshold, 0)
+ })
+ }
+
+ context('with balance connectors', () => {
+ const requestedAmount = 0
+ const prevConnectorId = '0x0000000000000000000000000000000000000000000000000000000000000001'
+
+ const setBalanceConnectors = () => {
+ beforeEach('set balance connectors', async () => {
+ const setBalanceConnectorsRole = task.interface.getSighash('setBalanceConnectors')
+ await authorizer.connect(owner).authorize(owner.address, task.address, setBalanceConnectorsRole, [])
+ await task.connect(owner).setBalanceConnectors(prevConnectorId, ZERO_BYTES32)
+ })
+
+ beforeEach('authorize task to update balance connectors', async () => {
+ const updateBalanceConnectorRole = smartVault.interface.getSighash('updateBalanceConnector')
+ await authorizer
+ .connect(owner)
+ .authorize(task.address, smartVault.address, updateBalanceConnectorRole, [])
+ })
+
+ beforeEach('assign amount in to previous balance connector', async () => {
+ const updateBalanceConnectorRole = smartVault.interface.getSighash('updateBalanceConnector')
+ await authorizer
+ .connect(owner)
+ .authorize(owner.address, smartVault.address, updateBalanceConnectorRole, [])
+ await smartVault.connect(owner).updateBalanceConnector(prevConnectorId, pool.address, amount, true)
+ })
+ }
+
+ const itUpdatesTheBalanceConnectorsProperly = () => {
+ it('updates the balance connectors properly', async () => {
+ const tx = await task.call(pool.address, requestedAmount)
+
+ await assertIndirectEvent(tx, smartVault.interface, 'BalanceConnectorUpdated', {
+ id: prevConnectorId,
+ token: pool.address,
+ amount,
+ added: false,
+ })
+ })
+ }
+
+ context('normal pools', () => {
+ const itExitsProportionally = () => {
+ const getTokenBalances = async (tokens: string[], account: Contract): Promise => {
+ return Promise.all(
+ tokens.map(async (tokenAddress: string) => {
+ const token = await instanceAt('IERC20', tokenAddress)
+ return token.balanceOf(account.address)
+ })
+ )
+ }
+
+ it('exits the BPT proportionally', async () => {
+ const { tokens } = await balancer.getPoolTokens(await pool.getPoolId())
+ const previousTokenBalances = await getTokenBalances(tokens, smartVault)
+ const previousBptBalance = await pool.balanceOf(smartVault.address)
+
+ await task.call(pool.address, requestedAmount)
+
+ const currentTokenBalances = await getTokenBalances(tokens, smartVault)
+ currentTokenBalances.forEach((currentBalance, i) =>
+ expect(currentBalance).to.be.gt(previousTokenBalances[i])
+ )
+
+ const currentBptBalance = await pool.balanceOf(smartVault.address)
+ expect(currentBptBalance).to.be.equal(previousBptBalance.sub(amount))
+ })
+ }
+
+ context('weighted pool', () => {
+ const POOL = '0x5c6Ee304399DBdB9C8Ef030aB642B10820DB8F56' // BAL-WETH 80/20
+ const WHALE = '0x24faf482304ed21f82c86ed5feb0ea313231a808'
+
+ setUpPool('IBalancerPool', POOL, WHALE)
+ setTokenThreshold()
+ setBalanceConnectors()
+
+ itExitsProportionally()
+ itUpdatesTheBalanceConnectorsProperly()
+ })
+
+ context('stable pool', () => {
+ const POOL = '0x06Df3b2bbB68adc8B0e302443692037ED9f91b42' // staBAL3
+ const WHALE = '0xb49d12163334f13c2a1619b6b73659fe6e849e30'
+
+ setUpPool('IBalancerPool', POOL, WHALE)
+ setTokenThreshold()
+ setBalanceConnectors()
+
+ itExitsProportionally()
+ itUpdatesTheBalanceConnectorsProperly()
+ })
+ })
+
+ context('boosted pools', () => {
+ const itSwapsForTheFirstUnderlyingToken = () => {
+ it('swaps to the first underlying token', async () => {
+ const bptIndex = await pool.getBptIndex()
+ const { tokens } = await balancer.getPoolTokens(await pool.getPoolId())
+ const underlying = await instanceAt('IBalancerBoostedPool', tokens[bptIndex.eq(0) ? 1 : 0])
+
+ const previousBptBalance = await pool.balanceOf(smartVault.address)
+ const previousUnderlyingBalance = await underlying.balanceOf(smartVault.address)
+
+ await task.call(pool.address, requestedAmount)
+
+ const currentBptBalance = await pool.balanceOf(smartVault.address)
+ expect(currentBptBalance).to.be.equal(previousBptBalance.sub(amount))
+
+ const currentUnderlyingBalance = await underlying.balanceOf(smartVault.address)
+ expect(currentUnderlyingBalance).to.be.gt(previousUnderlyingBalance)
+ })
+ }
+
+ context('linear pool', () => {
+ const POOL = '0x2BBf681cC4eb09218BEe85EA2a5d3D13Fa40fC0C' // bb-a-USDT
+ const WHALE = '0xc578d755cd56255d3ff6e92e1b6371ba945e3984'
+
+ setUpPool('IBalancerLinearPool', POOL, WHALE)
+ setTokenThreshold()
+ setBalanceConnectors()
+
+ it('swaps for the first main token', async () => {
+ const mainToken = await instanceAt('IERC20', pool.getMainToken())
+
+ const previousBptBalance = await pool.balanceOf(smartVault.address)
+ const previousMainTokenBalance = await mainToken.balanceOf(smartVault.address)
+
+ await task.call(pool.address, requestedAmount)
+
+ const currentBptBalance = await pool.balanceOf(smartVault.address)
+ expect(currentBptBalance).to.be.equal(previousBptBalance.sub(amount))
+
+ const currentMainTokenBalance = await mainToken.balanceOf(smartVault.address)
+ expect(currentMainTokenBalance).to.be.gt(previousMainTokenBalance)
+ })
+
+ itUpdatesTheBalanceConnectorsProperly()
+ })
+
+ context('phantom pool', () => {
+ const POOL = '0x7B50775383d3D6f0215A8F290f2C9e2eEBBEceb2' // bb-a-USDT bb-a-DAI bb-a-USDC
+ const WHALE = '0x575daf04615aef7272b388e3d7fac8adf1974173'
+
+ setUpPool('IBalancerBoostedPool', POOL, WHALE)
+ setTokenThreshold()
+ setBalanceConnectors()
+
+ itSwapsForTheFirstUnderlyingToken()
+ itUpdatesTheBalanceConnectorsProperly()
+ })
+
+ context('composable pool', () => {
+ const POOL = '0xA13a9247ea42D743238089903570127DdA72fE44' // bb-a-USD
+ const WHALE = '0x43b650399f2e4d6f03503f44042faba8f7d73470'
+
+ setUpPool('IBalancerBoostedPool', POOL, WHALE)
+ setTokenThreshold()
+ setBalanceConnectors()
+
+ itSwapsForTheFirstUnderlyingToken()
+ itUpdatesTheBalanceConnectorsProperly()
+ })
+ })
+ })
+ })
+
+ context('when the threshold has not passed', () => {
+ const threshold = amount.add(1)
+
+ beforeEach('set token threshold', async () => {
+ const setDefaultTokenThresholdRole = task.interface.getSighash('setDefaultTokenThreshold')
+ await authorizer.connect(owner).authorize(owner.address, task.address, setDefaultTokenThresholdRole, [])
+ await task.connect(owner).setDefaultTokenThreshold(pool.address, threshold, 0)
+ })
+
+ it('reverts', async () => {
+ await expect(task.call(pool.address, amount)).to.be.revertedWith('TaskTokenThresholdNotMet')
+ })
+ })
+ })
+
+ context('when the amount is zero', () => {
+ const amount = 0
+
+ it('reverts', async () => {
+ await expect(task.call(pool.address, amount)).to.be.revertedWith('TaskAmountZero')
+ })
+ })
+ })
+
+ context('when the token is zero', () => {
+ const token = ZERO_ADDRESS
+
+ it('reverts', async () => {
+ await expect(task.call(token, 0)).to.be.revertedWith('TaskTokenZero')
+ })
+ })
+ })
+
+ context('when the sender is not authorized', () => {
+ it('reverts', async () => {
+ await expect(task.call(ZERO_ADDRESS, 0)).to.be.revertedWith('AuthSenderNotAllowed')
+ })
+ })
+ })
+})