Skip to content

Commit

Permalink
Tasks: Implement handle over (#122)
Browse files Browse the repository at this point in the history
  • Loading branch information
facuspagnuolo authored Nov 2, 2023
1 parent 556e3db commit 3c95072
Show file tree
Hide file tree
Showing 4 changed files with 325 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
42 changes: 42 additions & 0 deletions packages/tasks/contracts/interfaces/primitives/IHandleOver.sol
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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;
}
98 changes: 98 additions & 0 deletions packages/tasks/contracts/primitives/HandleOver.sol
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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);
}
}
184 changes: 184 additions & 0 deletions packages/tasks/test/primitives/HandleOver.test.ts
Original file line number Diff line number Diff line change
@@ -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')
})
})
})
})

0 comments on commit 3c95072

Please sign in to comment.