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') + }) + }) + }) +})