diff --git a/packages/authorizer/contracts/AuthorizedHelpers.sol b/packages/authorizer/contracts/AuthorizedHelpers.sol
index fcd0c037..b90d9e46 100644
--- a/packages/authorizer/contracts/AuthorizedHelpers.sol
+++ b/packages/authorizer/contracts/AuthorizedHelpers.sol
@@ -136,4 +136,10 @@ contract AuthorizedHelpers {
r[3] = p4;
r[4] = p5;
}
+
+ function authParams(address p1, bytes32 p2) internal pure returns (uint256[] memory r) {
+ r = new uint256[](2);
+ r[0] = uint256(uint160(p1));
+ r[1] = uint256(p2);
+ }
}
diff --git a/packages/tasks/contracts/interfaces/swap/IBalancerV2Swapper.sol b/packages/tasks/contracts/interfaces/swap/IBalancerV2Swapper.sol
new file mode 100644
index 00000000..0eae4dda
--- /dev/null
+++ b/packages/tasks/contracts/interfaces/swap/IBalancerV2Swapper.sol
@@ -0,0 +1,37 @@
+// 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 './IBaseSwapTask.sol';
+
+/**
+ * @dev Balancer v2 swapper task interface
+ */
+interface IBalancerV2Swapper is IBaseSwapTask {
+ /**
+ * @dev The pool id for the token is not set
+ */
+ error TaskMissingPoolId();
+
+ /**
+ * @dev Emitted every time a pool is set for a token
+ */
+ event BalancerPoolIdSet(address indexed token, bytes32 poolId);
+
+ /**
+ * @dev Execution function
+ */
+ function call(address tokenIn, uint256 amountIn, uint256 slippage) external;
+}
diff --git a/packages/tasks/contracts/swap/BalancerV2Swapper.sol b/packages/tasks/contracts/swap/BalancerV2Swapper.sol
new file mode 100644
index 00000000..1f09e32b
--- /dev/null
+++ b/packages/tasks/contracts/swap/BalancerV2Swapper.sol
@@ -0,0 +1,152 @@
+// 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 '@mimic-fi/v3-helpers/contracts/math/FixedPoint.sol';
+import '@mimic-fi/v3-helpers/contracts/utils/BytesHelpers.sol';
+import '@mimic-fi/v3-connectors/contracts/interfaces/balancer/IBalancerV2SwapConnector.sol';
+
+import './BaseSwapTask.sol';
+import '../interfaces/swap/IBalancerV2Swapper.sol';
+import '../interfaces/liquidity/balancer/IBalancerPool.sol';
+
+/**
+ * @title Balancer v2 swapper task
+ * @dev Task that extends the base swap task to use Balancer
+ */
+contract BalancerV2Swapper is IBalancerV2Swapper, BaseSwapTask {
+ using FixedPoint for uint256;
+ using BytesHelpers for bytes;
+
+ // Execution type for relayers
+ bytes32 public constant override EXECUTION_TYPE = keccak256('BALANCER_V2_SWAPPER');
+
+ // List of pool id's per token
+ mapping (address => bytes32) public balancerPoolId;
+
+ /**
+ * @dev Balancer pool id config. Only used in the initializer.
+ */
+ struct BalancerPoolId {
+ address token;
+ bytes32 poolId;
+ }
+
+ /**
+ * @dev Balancer v2 swap config. Only used in the initalizer
+ */
+ struct BalancerV2SwapConfig {
+ BalancerPoolId[] balancerPoolIds;
+ BaseSwapConfig baseSwapConfig;
+ }
+
+ /**
+ * @dev Initializes the Balancer v2 swapper
+ * @param config Balancer v2 swap config
+ */
+ function initialize(BalancerV2SwapConfig memory config) external initializer {
+ __BalancerV2Swapper_init(config);
+ }
+
+ /**
+ * @dev Initializes the Balancer v2 swapper. It does call upper contracts.
+ * @param config Balancer v2 swap config
+ */
+ function __BalancerV2Swapper_init(BalancerV2SwapConfig memory config) internal onlyInitializing {
+ __BaseSwapTask_init(config.baseSwapConfig);
+ __BalancerV2Swapper_init_unchained(config);
+ }
+
+ /**
+ * @dev Initializes the Balancer swapper. It does not call upper contracts
+ * @param config Balancer V2 swap config
+ */
+ function __BalancerV2Swapper_init_unchained(BalancerV2SwapConfig memory config) internal onlyInitializing {
+ // solhint-disable-previous-line no-empty-blocks
+ }
+
+ /**
+ * @dev Sets a Balancer pool ID for a token
+ * @param token Address of the token to set the pool ID of
+ * @param poolId ID of the pool to be set for the given token
+ */
+ function setPoolId(address token, bytes32 poolId) external authP(authParams(token, poolId)) {
+ _setPoolId(token, poolId);
+ }
+
+ /**
+ * @dev Executes the Balancer v2 swapper task
+ */
+ function call(address tokenIn, uint256 amountIn, uint256 slippage)
+ external
+ override
+ authP(authParams(tokenIn, amountIn, slippage))
+ {
+ if (amountIn == 0) amountIn = getTaskAmount(tokenIn);
+ _beforeBalancerV2Swapper(tokenIn, amountIn, slippage);
+
+ address tokenOut = getTokenOut(tokenIn);
+ uint256 price = _getPrice(tokenIn, tokenOut);
+ uint256 minAmountOut = amountIn.mulUp(price).mulUp(FixedPoint.ONE - slippage);
+ bytes32 poolId = balancerPoolId[tokenIn];
+
+ bytes memory connectorData = abi.encodeWithSelector(
+ IBalancerV2SwapConnector.execute.selector,
+ tokenIn,
+ tokenOut,
+ amountIn,
+ minAmountOut,
+ poolId,
+ new bytes32[](0),
+ new address[](0)
+ );
+
+ bytes memory result = ISmartVault(smartVault).execute(connector, connectorData);
+ _afterBalancerV2Swapper(tokenIn, amountIn, slippage, tokenOut, result.toUint256());
+ }
+
+ /**
+ * @dev Before Balancer V2 swapper hook
+ */
+ function _beforeBalancerV2Swapper(address token, uint256 amount, uint256 slippage) internal virtual {
+ _beforeBaseSwapTask(token, amount, slippage);
+ if (balancerPoolId[token] == bytes32(0)) revert TaskMissingPoolId();
+ }
+
+ /**
+ * @dev After Balancer v2 swapper hook
+ */
+ function _afterBalancerV2Swapper(
+ address tokenIn,
+ uint256 amountIn,
+ uint256 slippage,
+ address tokenOut,
+ uint256 amountOut
+ ) internal virtual {
+ _afterBaseSwapTask(tokenIn, amountIn, slippage, tokenOut, amountOut);
+ }
+
+ /**
+ * @dev Sets a Balancer pool ID for a token
+ * @param token Address of the token to set the pool ID of
+ * @param poolId ID of the pool to be set for the given token
+ */
+ function _setPoolId(address token, bytes32 poolId) internal {
+ if (token == address(0)) revert TaskTokenZero();
+
+ balancerPoolId[token] = poolId;
+ emit BalancerPoolIdSet(token, poolId);
+ }
+}
diff --git a/packages/tasks/test/swap/BalancerV2Swapper.test.ts b/packages/tasks/test/swap/BalancerV2Swapper.test.ts
new file mode 100644
index 00000000..58ca1d73
--- /dev/null
+++ b/packages/tasks/test/swap/BalancerV2Swapper.test.ts
@@ -0,0 +1,108 @@
+import {
+ assertEvent,
+ deploy,
+ deployProxy,
+ deployTokenMock,
+ getSigners,
+ ONES_BYTES32,
+ ZERO_ADDRESS,
+} from '@mimic-fi/v3-helpers'
+import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address'
+import { expect } from 'chai'
+import { Contract } from 'ethers'
+
+import { buildEmptyTaskConfig, deployEnvironment } from '../../src/setup'
+import { itBehavesLikeBaseSwapTask } from './BaseSwapTask.behavior'
+
+describe('BalancerV2Swapper', () => {
+ let task: Contract
+ let smartVault: Contract, authorizer: Contract, connector: Contract, owner: SignerWithAddress
+
+ before('setup', async () => {
+ // eslint-disable-next-line prettier/prettier
+ [, owner] = await getSigners()
+ ;({ authorizer, smartVault } = await deployEnvironment(owner))
+ })
+
+ before('deploy connector', async () => {
+ connector = await deploy('BalancerV2SwapConnectorMock', [ZERO_ADDRESS])
+ const overrideConnectorCheckRole = smartVault.interface.getSighash('overrideConnectorCheck')
+ await authorizer.connect(owner).authorize(owner.address, smartVault.address, overrideConnectorCheckRole, [])
+ await smartVault.connect(owner).overrideConnectorCheck(connector.address, true)
+ })
+
+ beforeEach('deploy task', async () => {
+ task = await deployProxy(
+ 'BalancerV2Swapper',
+ [],
+ [
+ {
+ balancerPoolIds: [],
+ baseSwapConfig: {
+ connector: connector.address,
+ tokenOut: ZERO_ADDRESS,
+ maxSlippage: 0,
+ customTokensOut: [],
+ customMaxSlippages: [],
+ taskConfig: buildEmptyTaskConfig(owner, smartVault),
+ },
+ },
+ ]
+ )
+ })
+
+ describe('swapper', () => {
+ beforeEach('set params', async function () {
+ this.owner = owner
+ this.task = task
+ this.authorizer = authorizer
+ })
+
+ itBehavesLikeBaseSwapTask('BALANCER_V2_SWAPPER')
+ })
+
+ describe('setPoolId', () => {
+ let token: Contract
+
+ before('deploy token mock', async () => {
+ token = await deployTokenMock('TKN')
+ })
+
+ context('when the sender is authorized', () => {
+ beforeEach('set sender', async () => {
+ const setPoolIdRole = task.interface.getSighash('setPoolId')
+ await authorizer.connect(owner).authorize(owner.address, task.address, setPoolIdRole, [])
+ task = task.connect(owner)
+ })
+
+ context('when the pool id is not zero', () => {
+ const poolId = ONES_BYTES32
+
+ it('emits an event', async () => {
+ const tx = await task.setPoolId(token.address, poolId)
+ await assertEvent(tx, 'BalancerPoolIdSet', { token: token.address, poolId })
+ })
+
+ context('when modifying the pool id', () => {
+ beforeEach('set pool id', async () => {
+ await task.setPoolId(token.address, poolId)
+ })
+
+ it('updates the pool id', async () => {
+ const poolId = '0x0000000000000000000000000000000000000000000000000000000000000001'
+ const tx = await task.setPoolId(token.address, poolId)
+ await assertEvent(tx, 'BalancerPoolIdSet', { token: token.address, poolId })
+ })
+ })
+ })
+
+ context('when the token address is zero', () => {
+ it('reverts', async () => {
+ await expect(
+ task.setPoolId(ZERO_ADDRESS, '0x0000000000000000000000000000000000000000000000000000000000000001')
+ ).to.be.revertedWith('TaskTokenZero')
+ })
+ })
+ })
+ })
+})