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