diff --git a/packages/tasks/contracts/interfaces/primitives/ICollector.sol b/packages/tasks/contracts/interfaces/primitives/ICollector.sol
index 56afd626..d4bb4f16 100644
--- a/packages/tasks/contracts/interfaces/primitives/ICollector.sol
+++ b/packages/tasks/contracts/interfaces/primitives/ICollector.sol
@@ -52,7 +52,7 @@ interface ICollector is ITask {
function setTokensSource(address tokensSource) external;
/**
- * @dev Executes the withdrawer task
+ * @dev Executes the collector task
*/
function call(address token, uint256 amount) external;
}
diff --git a/packages/tasks/contracts/interfaces/primitives/IHandleOver.sol b/packages/tasks/contracts/interfaces/primitives/IHandleOver.sol
new file mode 100644
index 00000000..3db06ab6
--- /dev/null
+++ b/packages/tasks/contracts/interfaces/primitives/IHandleOver.sol
@@ -0,0 +1,42 @@
+// 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 Hand over task interface
+ */
+interface IHandleOver is ITask {
+ /**
+ * @dev The token is zero
+ */
+ error TaskTokenZero();
+
+ /**
+ * @dev The amount is zero
+ */
+ error TaskAmountZero();
+
+ /**
+ * @dev The tokens source is zero
+ */
+ error TaskConnectorZero(bytes32 id);
+
+ /**
+ * @dev Executes the hand over task
+ */
+ function call(address token, uint256 amount) external;
+}
diff --git a/packages/tasks/contracts/primitives/HandleOver.sol b/packages/tasks/contracts/primitives/HandleOver.sol
new file mode 100644
index 00000000..951fde3b
--- /dev/null
+++ b/packages/tasks/contracts/primitives/HandleOver.sol
@@ -0,0 +1,98 @@
+// 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/utils/ERC20Helpers.sol';
+
+import '../Task.sol';
+import '../interfaces/primitives/IHandleOver.sol';
+
+/**
+ * @title Hand over task
+ * @dev Task that simply moves tokens from one balance connector to the other
+ */
+contract HandleOver is IHandleOver, Task {
+ // Execution type for relayers
+ bytes32 public constant override EXECUTION_TYPE = keccak256('HANDLE_OVER');
+
+ /**
+ * @dev Hand over config. Only used in the initializer.
+ */
+ struct HandleOverConfig {
+ TaskConfig taskConfig;
+ }
+
+ /**
+ * @dev Initializes the hand over task
+ * @param config Hand over config
+ */
+ function initialize(HandleOverConfig memory config) external virtual initializer {
+ __HandleOver_init(config);
+ }
+
+ /**
+ * @dev Initializes the hand over task. It does call upper contracts initializers.
+ * @param config Hand over config
+ */
+ function __HandleOver_init(HandleOverConfig memory config) internal onlyInitializing {
+ __Task_init(config.taskConfig);
+ __HandleOver_init_unchained(config);
+ }
+
+ /**
+ * @dev Initializes the hand over task. It does not call upper contracts initializers.
+ * @param config Hand over config
+ */
+ function __HandleOver_init_unchained(HandleOverConfig memory config) internal onlyInitializing {
+ // solhint-disable-previous-line no-empty-blocks
+ }
+
+ /**
+ * @dev Execute the hand over taks
+ */
+ function call(address token, uint256 amount) external override authP(authParams(token, amount)) {
+ if (amount == 0) amount = getTaskAmount(token);
+ _beforeHandleOver(token, amount);
+ _afterHandleOver(token, amount);
+ }
+
+ /**
+ * @dev Before hand over task hook
+ */
+ function _beforeHandleOver(address token, uint256 amount) internal virtual {
+ _beforeTask(token, amount);
+ if (token == address(0)) revert TaskTokenZero();
+ if (amount == 0) revert TaskAmountZero();
+ }
+
+ /**
+ * @dev After hand over task hook
+ */
+ function _afterHandleOver(address token, uint256 amount) internal virtual {
+ _increaseBalanceConnector(token, amount);
+ _afterTask(token, amount);
+ }
+
+ /**
+ * @dev Sets the balance connectors. Both balance connector must be set.
+ * @param previous Balance connector id of the previous task in the workflow
+ * @param next Balance connector id of the next task in the workflow
+ */
+ function _setBalanceConnectors(bytes32 previous, bytes32 next) internal virtual override {
+ if (previous == bytes32(0)) revert TaskConnectorZero(previous);
+ if (next == bytes32(0)) revert TaskConnectorZero(next);
+ super._setBalanceConnectors(previous, next);
+ }
+}
diff --git a/packages/tasks/test/primitives/HandleOver.test.ts b/packages/tasks/test/primitives/HandleOver.test.ts
new file mode 100644
index 00000000..1ec85e54
--- /dev/null
+++ b/packages/tasks/test/primitives/HandleOver.test.ts
@@ -0,0 +1,184 @@
+import {
+ assertEvent,
+ assertIndirectEvent,
+ deployProxy,
+ deployTokenMock,
+ fp,
+ getSigners,
+ 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 { ethers } from 'hardhat'
+
+import { buildEmptyTaskConfig, deployEnvironment } from '../../src/setup'
+
+describe('HandleOver', () => {
+ const PREVIOUS = '0x0000000000000000000000000000000000000000000000000000000000000001'
+ const NEXT = '0x0000000000000000000000000000000000000000000000000000000000000002'
+
+ 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 () => {
+ const taskConfig = buildEmptyTaskConfig(owner, smartVault)
+ taskConfig.baseConfig.previousBalanceConnectorId = PREVIOUS
+ taskConfig.baseConfig.nextBalanceConnectorId = NEXT
+ task = await deployProxy('HandleOver', [], [{ taskConfig }])
+ })
+
+ describe('execution type', () => {
+ it('defines it correctly', async () => {
+ const expectedType = ethers.utils.solidityKeccak256(['string'], ['HANDLE_OVER'])
+ expect(await task.EXECUTION_TYPE()).to.be.equal(expectedType)
+ })
+ })
+
+ describe('setBalanceConnectors', () => {
+ context('when the sender is authorized', () => {
+ beforeEach('authorize sender', async () => {
+ const setBalanceConnectorsRole = task.interface.getSighash('setBalanceConnectors')
+ await authorizer.connect(owner).authorize(owner.address, task.address, setBalanceConnectorsRole, [])
+ task = task.connect(owner)
+ })
+
+ const itCanBeSet = (previous: string, next: string) => {
+ it('can be set', async () => {
+ const tx = await task.setBalanceConnectors(previous, next)
+
+ const connectors = await task.getBalanceConnectors()
+ expect(connectors.previous).to.be.equal(previous)
+ expect(connectors.next).to.be.equal(next)
+
+ await assertEvent(tx, 'BalanceConnectorsSet', { previous, next })
+ })
+ }
+
+ context('when setting next to non-zero', () => {
+ const next = NEXT
+
+ context('when setting previous to zero', () => {
+ const previous = PREVIOUS
+
+ itCanBeSet(previous, next)
+ })
+
+ context('when setting previous to non-zero', () => {
+ const previous = ZERO_BYTES32
+
+ it('reverts', async () => {
+ await expect(task.setBalanceConnectors(previous, next)).to.be.revertedWith('TaskConnectorZero')
+ })
+ })
+ })
+
+ context('when setting next to zero', () => {
+ const next = ZERO_BYTES32
+
+ context('when setting previous to zero', () => {
+ const previous = ZERO_BYTES32
+
+ it('reverts', async () => {
+ await expect(task.setBalanceConnectors(previous, next)).to.be.revertedWith('TaskConnectorZero')
+ })
+ })
+
+ context('when setting previous to non-zero', () => {
+ const previous = PREVIOUS
+
+ it('reverts', async () => {
+ await expect(task.setBalanceConnectors(previous, next)).to.be.revertedWith('TaskConnectorZero')
+ })
+ })
+ })
+ })
+
+ context('when the sender is not authorized', () => {
+ it('reverts', async () => {
+ await expect(task.setBalanceConnectors(ZERO_BYTES32, ZERO_BYTES32)).to.be.revertedWith('AuthSenderNotAllowed')
+ })
+ })
+ })
+
+ describe('call', () => {
+ let token: Contract
+ const amount = fp(20)
+
+ beforeEach('deploy token', async () => {
+ token = await deployTokenMock('USDC')
+ await token.mint(smartVault.address, amount)
+ })
+
+ beforeEach('update 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(PREVIOUS, token.address, amount, true)
+ })
+
+ beforeEach('authorize task to update balance connectors', async () => {
+ const updateBalanceConnectorRole = smartVault.interface.getSighash('updateBalanceConnector')
+ await authorizer.connect(owner).authorize(task.address, smartVault.address, updateBalanceConnectorRole, [])
+ })
+
+ 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)
+ })
+
+ const itExecutesTheTaskProperly = (requestedAmount: BigNumber) => {
+ it('emits an Executed event', async () => {
+ const tx = await task.call(token.address, requestedAmount)
+
+ await assertEvent(tx, 'Executed')
+ })
+
+ it('updates the balance connectors properly', async () => {
+ const transactedAmount = requestedAmount.eq(fp(0)) ? amount : requestedAmount
+
+ const tx = await task.call(token.address, requestedAmount)
+
+ await assertIndirectEvent(tx, smartVault.interface, 'BalanceConnectorUpdated', {
+ id: PREVIOUS,
+ token,
+ amount: transactedAmount,
+ added: false,
+ })
+
+ await assertIndirectEvent(tx, smartVault.interface, 'BalanceConnectorUpdated', {
+ id: NEXT,
+ token,
+ amount: transactedAmount,
+ added: true,
+ })
+ })
+ }
+
+ context('when requesting a specific amount', () => {
+ const requestedAmount = fp(0)
+
+ itExecutesTheTaskProperly(requestedAmount)
+ })
+
+ context('when requesting a specific amount', () => {
+ const requestedAmount = amount.div(2)
+
+ itExecutesTheTaskProperly(requestedAmount)
+ })
+ })
+
+ context('when the sender is not authorized', () => {
+ it('reverts', async () => {
+ await expect(task.call(token.address, 0)).to.be.revertedWith('AuthSenderNotAllowed')
+ })
+ })
+ })
+})