diff --git a/packages/authorizer/contracts/AuthorizedHelpers.sol b/packages/authorizer/contracts/AuthorizedHelpers.sol
index 04bf69bc..b6d504a7 100644
--- a/packages/authorizer/contracts/AuthorizedHelpers.sol
+++ b/packages/authorizer/contracts/AuthorizedHelpers.sol
@@ -120,4 +120,22 @@ contract AuthorizedHelpers {
r[3] = p4;
r[4] = p5;
}
+
+ function authParams(address p1, uint256 p2, uint256 p3, uint24 p4, address[] memory p5, uint24[] memory p6)
+ internal
+ pure
+ returns (uint256[] memory r)
+ {
+ r = new uint256[](4 + p5.length + p6.length);
+ r[0] = uint256(uint160(p1));
+ r[1] = p2;
+ r[2] = p3;
+ r[3] = p4;
+ for (uint256 i = 0; i < p5.length; i++) {
+ r[i + 4] = uint256(uint160(p5[i]));
+ }
+ for (uint256 i = 0; i < p6.length; i++) {
+ r[i + 4 + p5.length] = p6[i];
+ }
+ }
}
diff --git a/packages/connectors/contracts/swap/uniswap-v3/UniswapV3Connector.sol b/packages/connectors/contracts/swap/uniswap-v3/IUniswapV3Connector.sol
similarity index 100%
rename from packages/connectors/contracts/swap/uniswap-v3/UniswapV3Connector.sol
rename to packages/connectors/contracts/swap/uniswap-v3/IUniswapV3Connector.sol
diff --git a/packages/tasks/contracts/interfaces/swap/IUniswapV3Swapper.sol b/packages/tasks/contracts/interfaces/swap/IUniswapV3Swapper.sol
new file mode 100644
index 00000000..1961db75
--- /dev/null
+++ b/packages/tasks/contracts/interfaces/swap/IUniswapV3Swapper.sol
@@ -0,0 +1,34 @@
+// 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 UniSwap v3 swapper action interface
+ */
+interface IUniswapV3Swapper is IBaseSwapTask {
+ /**
+ * @dev Execution function
+ */
+ function call(
+ address tokenIn,
+ uint256 amountIn,
+ uint256 minAmountOut,
+ uint24 fee,
+ address[] memory hopTokens,
+ uint24[] memory hopFees
+ ) external;
+}
diff --git a/packages/tasks/contracts/swap/UniswapV3Swapper.sol b/packages/tasks/contracts/swap/UniswapV3Swapper.sol
new file mode 100644
index 00000000..166ffcdc
--- /dev/null
+++ b/packages/tasks/contracts/swap/UniswapV3Swapper.sol
@@ -0,0 +1,115 @@
+// 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/swap/uniswap-v3/IUniswapV3Connector.sol';
+
+import './BaseSwapTask.sol';
+import '../interfaces/swap/IUniswapV3Swapper.sol';
+
+contract UniswapV3Swapper is IUniswapV3Swapper, BaseSwapTask {
+ using FixedPoint for uint256;
+ using BytesHelpers for bytes;
+
+ // Execution type for relayers
+ bytes32 public constant override EXECUTION_TYPE = keccak256('UNISWAP_V3_SWAPPER');
+
+ /**
+ * @dev Uniswap v3 swapper task config. Only used in the initializer.
+ */
+ struct UniswapV3SwapperConfig {
+ BaseSwapConfig baseSwapConfig;
+ }
+
+ /**
+ * @dev Initializes the Uniswap v3 swapper action.
+ * @param config Uniswap v3 swap config.
+ */
+ function initialize(UniswapV3SwapperConfig memory config) external initializer {
+ __UniswapV3Swapper_init(config);
+ }
+
+ /**
+ * @dev Initializes the Uniswap V3 swapper. It does call upper contracts.
+ * @param config Uniswap v3 swap config.
+ */
+ function __UniswapV3Swapper_init(UniswapV3SwapperConfig memory config) internal onlyInitializing {
+ __BaseSwapTask_init(config.baseSwapConfig);
+ __UniswapV3Swapper_init_unchained(config);
+ }
+
+ /**
+ * @dev Initilizes the uniswap V3 swapper. It does not call upper contracts.
+ * @param config Uniswap v3 swap config.
+ */
+ function __UniswapV3Swapper_init_unchained(UniswapV3SwapperConfig memory config) internal onlyInitializing {
+ // solhint-disable-previous-line no-empty-blocks
+ }
+
+ /**
+ * @dev Executes the Uniswap v3 swapper task
+ */
+ function call(
+ address tokenIn,
+ uint256 amountIn,
+ uint256 slippage,
+ uint24 fee,
+ address[] memory hopTokens,
+ uint24[] memory hopFees
+ ) external override authP(authParams(tokenIn, amountIn, slippage, fee, hopTokens, hopFees)) {
+ if (amountIn == 0) amountIn = getTaskAmount(tokenIn);
+ _beforeUniswapV3Swapper(tokenIn, amountIn, slippage);
+
+ address tokenOut = getTokenOut(tokenIn);
+ uint256 price = _getPrice(tokenIn, tokenOut);
+ uint256 minAmountOut = amountIn.mulUp(price).mulUp(FixedPoint.ONE - slippage);
+
+ bytes memory connectorData = abi.encodeWithSelector(
+ UniswapV3Connector.execute.selector,
+ tokenIn,
+ tokenOut,
+ amountIn,
+ minAmountOut,
+ fee,
+ hopTokens,
+ hopFees
+ );
+
+ bytes memory result = ISmartVault(smartVault).execute(connector, connectorData);
+ _afterUniswapV3Swapper(tokenIn, amountIn, slippage, tokenOut, result.toUint256());
+ }
+
+ /**
+ * @dev Before Uniswap v3Swapper Task
+ */
+ function _beforeUniswapV3Swapper(address token, uint256 amount, uint256 slippage) internal virtual {
+ _beforeBaseSwapTask(token, amount, slippage);
+ }
+
+ /**
+ * @dev After Uniswap V3 swapper hook
+ */
+ function _afterUniswapV3Swapper(
+ address tokenIn,
+ uint256 amountIn,
+ uint256 slippage,
+ address tokenOut,
+ uint256 amountOut
+ ) internal virtual {
+ _afterBaseSwapTask(tokenIn, amountIn, slippage, tokenOut, amountOut);
+ }
+}
diff --git a/packages/tasks/contracts/test/swap/UniswapV3ConnectorMock.sol b/packages/tasks/contracts/test/swap/UniswapV3ConnectorMock.sol
new file mode 100644
index 00000000..3d6fec76
--- /dev/null
+++ b/packages/tasks/contracts/test/swap/UniswapV3ConnectorMock.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.
+
+pragma solidity ^0.8.0;
+
+contract UniswapV3ConnectorMock {
+ event LogExecute(
+ address tokenIn,
+ address tokenOut,
+ uint256 amountIn,
+ uint256 minAmountOut,
+ uint24 fee,
+ address[] hopTokens,
+ uint24[] hopFees
+ );
+
+ function execute(
+ address tokenIn,
+ address tokenOut,
+ uint256 amountIn,
+ uint256 minAmountOut,
+ uint24 fee,
+ address[] memory hopTokens,
+ uint24[] memory hopFees
+ ) external returns (uint256) {
+ emit LogExecute(tokenIn, tokenOut, amountIn, minAmountOut, fee, hopTokens, hopFees);
+ return minAmountOut;
+ }
+}
diff --git a/packages/tasks/test/swap/UniswapV3Swapper.test.ts b/packages/tasks/test/swap/UniswapV3Swapper.test.ts
new file mode 100644
index 00000000..1920cba7
--- /dev/null
+++ b/packages/tasks/test/swap/UniswapV3Swapper.test.ts
@@ -0,0 +1,506 @@
+import { OP } from '@mimic-fi/v3-authorizer'
+import {
+ assertIndirectEvent,
+ assertNoEvent,
+ BigNumberish,
+ deploy,
+ deployFeedMock,
+ deployProxy,
+ deployTokenMock,
+ fp,
+ getSigners,
+ MAX_UINT256,
+ ZERO_ADDRESS,
+} from '@mimic-fi/v3-helpers'
+import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address'
+import { expect } from 'chai'
+import { Contract, ContractTransaction } from 'ethers'
+import { defaultAbiCoder } from 'ethers/lib/utils'
+import { ethers } from 'hardhat'
+
+import { buildEmptyTaskConfig, deployEnvironment } from '../../src/setup'
+import { itBehavesLikeBaseSwapTask } from './BaseSwapTask.behavior'
+
+describe('UniswapV3Swapper', () => {
+ let task: Contract
+ let smartVault: Contract, authorizer: Contract, priceOracle: Contract, connector: Contract, owner: SignerWithAddress
+
+ before('setup', async () => {
+ // eslint-disable-next-line prettier/prettier
+ [, owner] = await getSigners()
+ ;({ authorizer, smartVault, priceOracle } = await deployEnvironment(owner))
+ })
+
+ before('deploy connector', async () => {
+ connector = await deploy('UniswapV3ConnectorMock')
+ 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(
+ 'UniswapV3Swapper',
+ [],
+ [
+ {
+ 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('UNISWAP_V3_SWAPPER')
+ })
+
+ describe('call', () => {
+ beforeEach('authorize task', async () => {
+ const executeRole = smartVault.interface.getSighash('execute')
+ const params = [{ op: OP.EQ, value: connector.address }]
+ await authorizer.connect(owner).authorize(task.address, smartVault.address, executeRole, params)
+ })
+
+ 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 in is not the zero address', () => {
+ let tokenIn: Contract
+
+ beforeEach('set token in', async () => {
+ tokenIn = await deployTokenMock('TKN')
+ })
+
+ context('when the amount in is not zero', () => {
+ const tokenRate = 2 // 1 token in = 2 token out
+ const thresholdAmount = fp(0.1) // in token out
+ const maxThresholdAmount = fp(0.0)
+ const thresholdAmountInTokenIn = thresholdAmount.div(tokenRate) // threshold expressed in token in
+ const amountIn = thresholdAmountInTokenIn
+
+ context('when the token in is allowed', () => {
+ context('when there is a token out set', () => {
+ let tokenOut: Contract,
+ extraCallData = ''
+
+ beforeEach('set default token out', async () => {
+ tokenOut = await deployTokenMock('TKN')
+ const setDefaultTokenOutRole = task.interface.getSighash('setDefaultTokenOut')
+ await authorizer.connect(owner).authorize(owner.address, task.address, setDefaultTokenOutRole, [])
+ await task.connect(owner).setDefaultTokenOut(tokenOut.address)
+ })
+
+ context('when an off-chain oracle is given', () => {
+ beforeEach('sign off-chain oracle', async () => {
+ const setSignerRole = priceOracle.interface.getSighash('setSigner')
+ await authorizer.connect(owner).authorize(owner.address, priceOracle.address, setSignerRole, [])
+ await priceOracle.connect(owner).setSigner(owner.address, true)
+
+ type PriceData = { base: string; quote: string; rate: BigNumberish; deadline: BigNumberish }
+ const pricesData: PriceData[] = [
+ {
+ base: tokenIn.address,
+ quote: tokenOut.address,
+ rate: fp(tokenRate),
+ deadline: MAX_UINT256,
+ },
+ {
+ base: tokenOut.address,
+ quote: tokenIn.address,
+ rate: fp(1).mul(fp(1)).div(fp(tokenRate)),
+ deadline: MAX_UINT256,
+ },
+ ]
+
+ const PricesDataType = 'PriceData(address base, address quote, uint256 rate, uint256 deadline)[]'
+ const encodedPrices = await defaultAbiCoder.encode([PricesDataType], [pricesData])
+ const message = ethers.utils.solidityKeccak256(['bytes'], [encodedPrices])
+ const signature = await owner.signMessage(ethers.utils.arrayify(message))
+ const data = defaultAbiCoder.encode([PricesDataType, 'bytes'], [pricesData, signature]).slice(2)
+ const dataLength = defaultAbiCoder.encode(['uint256'], [data.length / 2]).slice(2)
+ extraCallData = `${data}${dataLength}`
+ })
+
+ beforeEach('set threshold', async () => {
+ const setDefaultTokenThresholdRole = task.interface.getSighash('setDefaultTokenThreshold')
+ await authorizer
+ .connect(owner)
+ .authorize(owner.address, task.address, setDefaultTokenThresholdRole, [])
+ await task.connect(owner).setDefaultTokenThreshold(tokenOut.address, thresholdAmount, 0)
+ })
+
+ const executeTask = async (
+ amountIn,
+ slippage,
+ fee,
+ hopTokens,
+ hopFees
+ ): Promise => {
+ const callTx = await task.populateTransaction.call(
+ tokenIn.address,
+ amountIn,
+ slippage,
+ fee,
+ hopTokens,
+ hopFees
+ )
+ const callData = `${callTx.data}${extraCallData}`
+ return owner.sendTransaction({ to: task.address, data: callData })
+ }
+
+ context('when the smart vault balance passes the threshold', () => {
+ beforeEach('fund smart vault', async () => {
+ await tokenIn.mint(smartVault.address, amountIn)
+ })
+
+ context('when the slippage is below the limit', () => {
+ const hopTokens = []
+ const hopFees = []
+ const fee = 1
+ const slippage = fp(0.01)
+ const expectedAmountOut = amountIn.mul(tokenRate)
+ const minAmountOut = expectedAmountOut.mul(fp(1).sub(slippage)).div(fp(1))
+
+ beforeEach('set max slippage', async () => {
+ const setDefaultMaxSlippageRole = task.interface.getSighash('setDefaultMaxSlippage')
+ await authorizer
+ .connect(owner)
+ .authorize(owner.address, task.address, setDefaultMaxSlippageRole, [])
+ await task.connect(owner).setDefaultMaxSlippage(slippage)
+ })
+ const itExecutesTheTaskProperly = (requestedAmount: BigNumberish) => {
+ it('executes the expected connector', async () => {
+ const tx = await executeTask(requestedAmount, slippage, fee, hopTokens, hopFees)
+ const connectorData = connector.interface.encodeFunctionData('execute', [
+ tokenIn.address,
+ tokenOut.address,
+ amountIn,
+ minAmountOut,
+ fee,
+ hopTokens,
+ hopFees,
+ ])
+
+ await assertIndirectEvent(tx, smartVault.interface, 'Executed', {
+ connector,
+ data: connectorData,
+ })
+
+ await assertIndirectEvent(tx, connector.interface, 'LogExecute', {
+ tokenIn,
+ tokenOut,
+ amountIn,
+ minAmountOut,
+ hopTokens,
+ hopFees,
+ })
+ })
+
+ it('emits an Executed event', async () => {
+ const tx = await executeTask(requestedAmount, slippage, fee, hopTokens, hopFees)
+
+ await assertIndirectEvent(tx, task.interface, 'Executed')
+ })
+ }
+
+ context('without balance connectors', () => {
+ const requestedAmount = amountIn
+
+ itExecutesTheTaskProperly(requestedAmount)
+
+ it('does not update any balance connectors', async () => {
+ const tx = await executeTask(requestedAmount, slippage, fee, hopTokens, hopFees)
+
+ await assertNoEvent(tx, 'BalanceConnectorUpdated')
+ })
+ })
+
+ context('with balance connectors', () => {
+ const requestedAmount = 0
+ const prevConnectorId = '0x0000000000000000000000000000000000000000000000000000000000000001'
+ const nextConnectorId = '0x0000000000000000000000000000000000000000000000000000000000000002'
+
+ 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, nextConnectorId)
+ })
+
+ 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, tokenIn.address, amountIn, true)
+ })
+
+ itExecutesTheTaskProperly(requestedAmount)
+
+ it('updates the balance connectors properly', async () => {
+ const tx = await executeTask(requestedAmount, slippage, fee, hopTokens, hopFees)
+
+ await assertIndirectEvent(tx, smartVault.interface, 'BalanceConnectorUpdated', {
+ id: prevConnectorId,
+ token: tokenIn.address,
+ amount: amountIn,
+ added: false,
+ })
+
+ await assertIndirectEvent(tx, smartVault.interface, 'BalanceConnectorUpdated', {
+ id: nextConnectorId,
+ token: tokenOut.address,
+ amount: minAmountOut,
+ added: true,
+ })
+ })
+ })
+ })
+
+ context('when the slippage is above the limit', () => {
+ const slippage = fp(0.01)
+ const hopTokens = []
+ const hopFees = []
+ const fee = 1
+
+ it('reverts', async () => {
+ await expect(executeTask(amountIn, slippage, fee, hopTokens, hopFees)).to.be.revertedWith(
+ 'TaskSlippageAboveMax'
+ )
+ })
+ })
+ })
+
+ context('when the smart vault balance does not pass the threshold', () => {
+ const amountIn = thresholdAmountInTokenIn.div(2)
+ const hopTokens = []
+ const hopFees = []
+ const fee = 1
+
+ beforeEach('fund smart vault', async () => {
+ await tokenIn.mint(smartVault.address, amountIn)
+ })
+
+ it('reverts', async () => {
+ await expect(executeTask(amountIn, 0, fee, hopTokens, hopFees)).to.be.revertedWith(
+ 'TaskTokenThresholdNotMet'
+ )
+ })
+ })
+ })
+
+ context('when no off-chain oracle is given', () => {
+ context('when an on-chain oracle is given', () => {
+ beforeEach('set price feed', async () => {
+ const feed = await deployFeedMock(fp(tokenRate), 18)
+ const setFeedRole = priceOracle.interface.getSighash('setFeed')
+ await authorizer.connect(owner).authorize(owner.address, priceOracle.address, setFeedRole, [])
+ await priceOracle.connect(owner).setFeed(tokenIn.address, tokenOut.address, feed.address)
+ })
+
+ beforeEach('set threshold', async () => {
+ const setDefaultTokenThresholdRole = task.interface.getSighash('setDefaultTokenThreshold')
+ await authorizer
+ .connect(owner)
+ .authorize(owner.address, task.address, setDefaultTokenThresholdRole, [])
+ await task
+ .connect(owner)
+ .setDefaultTokenThreshold(tokenOut.address, thresholdAmount, maxThresholdAmount)
+ })
+
+ context('when the smart vault balance passes the threshold', () => {
+ beforeEach('fund smart vault', async () => {
+ await tokenIn.mint(smartVault.address, amountIn)
+ })
+
+ context('when the slippage is below the limit', () => {
+ const hopTokens = []
+ const hopFees = []
+ const slippage = fp(0.01)
+ const expectedAmountOut = amountIn.mul(tokenRate)
+ const minAmountOut = expectedAmountOut.mul(fp(1).sub(slippage)).div(fp(1))
+ const fee = 1
+
+ beforeEach('set max slippage', async () => {
+ const setDefaultMaxSlippageRole = task.interface.getSighash('setDefaultMaxSlippage')
+ await authorizer
+ .connect(owner)
+ .authorize(owner.address, task.address, setDefaultMaxSlippageRole, [])
+ await task.connect(owner).setDefaultMaxSlippage(slippage)
+ })
+
+ it('executes the expected connector', async () => {
+ const tx = await task.call(tokenIn.address, amountIn, slippage, fee, hopTokens, hopFees)
+
+ const connectorData = connector.interface.encodeFunctionData('execute', [
+ tokenIn.address,
+ tokenOut.address,
+ amountIn,
+ minAmountOut,
+ fee,
+ hopTokens,
+ hopFees,
+ ])
+
+ await assertIndirectEvent(tx, smartVault.interface, 'Executed', {
+ connector,
+ data: connectorData,
+ })
+
+ await assertIndirectEvent(tx, connector.interface, 'LogExecute', {
+ tokenIn,
+ tokenOut,
+ amountIn,
+ minAmountOut,
+ fee,
+ hopTokens,
+ hopFees,
+ })
+ })
+
+ it('emits an Executed event', async () => {
+ const tx = await task.call(tokenIn.address, amountIn, slippage, fee, hopTokens, hopFees)
+
+ await assertIndirectEvent(tx, task.interface, 'Executed')
+ })
+ })
+
+ context('when the slippage is above the limit', () => {
+ const slippage = fp(0.01)
+ const hopTokens = []
+ const hopFees = []
+ const fee = 1
+
+ it('reverts', async () => {
+ await expect(
+ task.call(tokenIn.address, amountIn, slippage, fee, hopTokens, hopFees)
+ ).to.be.revertedWith('TaskSlippageAboveMax')
+ })
+ })
+ })
+
+ context('when the smart vault balance does not pass the threshold', () => {
+ const amountIn = thresholdAmountInTokenIn.div(2)
+ const hopTokens = []
+ const hopFees = []
+ const fee = 1
+
+ beforeEach('fund smart vault', async () => {
+ await tokenIn.mint(smartVault.address, amountIn)
+ })
+
+ it('reverts', async () => {
+ await expect(task.call(tokenIn.address, amountIn, 0, fee, hopTokens, hopFees)).to.be.revertedWith(
+ 'TaskTokenThresholdNotMet'
+ )
+ })
+ })
+ })
+
+ context('when no on-chain oracle is given', () => {
+ const fee = 1
+ const hopTokens = []
+ const hopFees = []
+ it('reverts', async () => {
+ // TODO: Hardhat does not decode price oracle error properly
+ await expect(task.call(tokenIn.address, amountIn, 0, 1, fee, hopFees, hopTokens)).to.be.reverted
+ })
+ })
+ })
+ })
+
+ context('when the token out is not set', () => {
+ const fee = 1
+ const hopTokens = []
+ const hopFees = []
+ it('reverts', async () => {
+ await expect(task.call(tokenIn.address, amountIn, 0, fee, hopTokens, hopFees)).to.be.revertedWith(
+ 'TaskTokenOutNotSet'
+ )
+ })
+ })
+ })
+
+ context('when the token in is denied', () => {
+ const fee = 1
+ const hopTokens = []
+ const hopFees = []
+
+ beforeEach('deny token in', async () => {
+ const setTokensAcceptanceListRole = task.interface.getSighash('setTokensAcceptanceList')
+ await authorizer.connect(owner).authorize(owner.address, task.address, setTokensAcceptanceListRole, [])
+ await task.connect(owner).setTokensAcceptanceList([tokenIn.address], [true])
+ })
+
+ it('reverts', async () => {
+ await expect(task.call(tokenIn.address, 0, 0, fee, hopTokens, hopFees)).to.be.revertedWith(
+ 'TaskTokenNotAllowed'
+ )
+ })
+ })
+ })
+
+ context('when the amount in is zero', () => {
+ const amountIn = 0
+ const fee = 1
+ const hopTokens = []
+ const hopFees = []
+
+ it('reverts', async () => {
+ await expect(task.call(tokenIn.address, amountIn, 0, fee, hopTokens, hopFees)).to.be.revertedWith(
+ 'TaskAmountZero'
+ )
+ })
+ })
+ })
+
+ context('when the token in is the zero address', () => {
+ const tokenIn = ZERO_ADDRESS
+ const fee = 1
+ const hopTokens = []
+ const hopFees = []
+
+ it('reverts', async () => {
+ await expect(task.call(tokenIn, 0, 0, fee, hopTokens, hopFees)).to.be.revertedWith('TaskTokenZero')
+ })
+ })
+ })
+
+ context('when the sender is not authorized', () => {
+ const fee = 1
+ const hopTokens = []
+ const hopFees = []
+
+ it('reverts', async () => {
+ await expect(task.call(ZERO_ADDRESS, 0, 0, fee, hopTokens, hopFees)).to.be.revertedWith('AuthSenderNotAllowed')
+ })
+ })
+ })
+})