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