diff --git a/packages/authorizer/contracts/AuthorizedHelpers.sol b/packages/authorizer/contracts/AuthorizedHelpers.sol index 7a8a2ed6..fcd0c037 100644 --- a/packages/authorizer/contracts/AuthorizedHelpers.sol +++ b/packages/authorizer/contracts/AuthorizedHelpers.sol @@ -100,6 +100,14 @@ contract AuthorizedHelpers { r[3] = p4; } + function authParams(address p1, uint256 p2, address p3, uint256 p4) internal pure returns (uint256[] memory r) { + r = new uint256[](4); + r[0] = uint256(uint160(p1)); + r[1] = p2; + r[2] = uint256(uint160(p3)); + r[3] = p4; + } + function authParams(address p1, uint256 p2, uint256 p3, uint256 p4) internal pure returns (uint256[] memory r) { r = new uint256[](4); r[0] = uint256(uint160(p1)); diff --git a/packages/authorizer/contracts/test/AuthorizedHelpersMock.sol b/packages/authorizer/contracts/test/AuthorizedHelpersMock.sol index 503f38da..5c8af2f1 100644 --- a/packages/authorizer/contracts/test/AuthorizedHelpersMock.sol +++ b/packages/authorizer/contracts/test/AuthorizedHelpersMock.sol @@ -53,6 +53,10 @@ contract AuthorizedHelpersMock is AuthorizedHelpers { return authParams(p1, p2, p3, p4); } + function getAuthParams(address p1, uint256 p2, address p3, uint256 p4) external pure returns (uint256[] memory r) { + return authParams(p1, p2, p3, p4); + } + function getAuthParams(address p1, uint256 p2, uint256 p3, uint256 p4) external pure returns (uint256[] memory r) { return authParams(p1, p2, p3, p4); } diff --git a/packages/authorizer/test/AuthorizedHelpers.test.ts b/packages/authorizer/test/AuthorizedHelpers.test.ts index f3d52fcc..34b74161 100644 --- a/packages/authorizer/test/AuthorizedHelpers.test.ts +++ b/packages/authorizer/test/AuthorizedHelpers.test.ts @@ -56,6 +56,7 @@ describe('AuthorizedHelpers', () => { context('when the number of arguments is 4', () => { itBehavesLikeAuthParams('address,address,uint256,uint256') + itBehavesLikeAuthParams('address,uint256,address,uint256') itBehavesLikeAuthParams('address,uint256,uint256,uint256') itBehavesLikeAuthParams('bytes32,address,uint256,bool') }) diff --git a/packages/tasks/contracts/interfaces/liquidity/erc4626/IBaseERC4626Task.sol b/packages/tasks/contracts/interfaces/liquidity/erc4626/IBaseERC4626Task.sol new file mode 100644 index 00000000..bb1d0aa0 --- /dev/null +++ b/packages/tasks/contracts/interfaces/liquidity/erc4626/IBaseERC4626Task.sol @@ -0,0 +1,53 @@ +// 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 Base ERC4626 task interface + */ +interface IBaseERC4626Task is ITask { + /** + * @dev The token is zero + */ + error TaskTokenZero(); + + /** + * @dev The amount is zero + */ + error TaskAmountZero(); + + /** + * @dev The connector is zero + */ + error TaskConnectorZero(); + + /** + * @dev Emitted every time the connector is set + */ + event ConnectorSet(address indexed connector); + + /** + * @dev Tells the connector tied to the task + */ + function connector() external view returns (address); + + /** + * @dev Sets a new connector + * @param newConnector Address of the connector to be set + */ + function setConnector(address newConnector) external; +} diff --git a/packages/tasks/contracts/interfaces/liquidity/erc4626/IERC4626Exiter.sol b/packages/tasks/contracts/interfaces/liquidity/erc4626/IERC4626Exiter.sol new file mode 100644 index 00000000..60a380d1 --- /dev/null +++ b/packages/tasks/contracts/interfaces/liquidity/erc4626/IERC4626Exiter.sol @@ -0,0 +1,27 @@ +// 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 './IBaseERC4626Task.sol'; + +/** + * @dev ERC4626 exiter task interface + */ +interface IERC4626Exiter is IBaseERC4626Task { + /** + * @dev Executes the ERC4626 exiter task + */ + function call(address erc4626, uint256 amount, uint256 minAmountOut) external; +} diff --git a/packages/tasks/contracts/interfaces/liquidity/erc4626/IERC4626Joiner.sol b/packages/tasks/contracts/interfaces/liquidity/erc4626/IERC4626Joiner.sol new file mode 100644 index 00000000..a998f1ee --- /dev/null +++ b/packages/tasks/contracts/interfaces/liquidity/erc4626/IERC4626Joiner.sol @@ -0,0 +1,32 @@ +// 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 './IBaseERC4626Task.sol'; + +/** + * @dev ERC4626 joiner task interface + */ +interface IERC4626Joiner is IBaseERC4626Task { + /** + * The ERC4626 reference is zero + */ + error TaskERC4626Zero(); + + /** + * @dev Executes the ERC4626 joiner task + */ + function call(address token, uint256 amount, address erc4626, uint256 minAmountOut) external; +} diff --git a/packages/tasks/contracts/liquidity/erc4626/BaseERC4626Task.sol b/packages/tasks/contracts/liquidity/erc4626/BaseERC4626Task.sol new file mode 100644 index 00000000..684997d2 --- /dev/null +++ b/packages/tasks/contracts/liquidity/erc4626/BaseERC4626Task.sol @@ -0,0 +1,90 @@ +// 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 '../../Task.sol'; +import '../../interfaces/liquidity/erc4626/IBaseERC4626Task.sol'; + +/** + * @title Base ERC4626 task + * @dev Task that offers the basic components for more detailed ERC4626 related tasks + */ +abstract contract BaseERC4626Task is IBaseERC4626Task, Task { + // Task connector address + address public override connector; + + /** + * @dev Base ERC4626 config. Only used in the initializer. + */ + struct BaseERC4626Config { + address connector; + TaskConfig taskConfig; + } + + /** + * @dev Initializes the base ERC4626 task. It does call upper contracts initializers. + * @param config Base ERC4626 config + */ + function __BaseERC4626Task_init(BaseERC4626Config memory config) internal onlyInitializing { + __Task_init(config.taskConfig); + __BaseERC4626Task_init_unchained(config); + } + + /** + * @dev Initializes the base ERC4626 task. It does not call upper contracts initializers. + * @param config Base ERC4626 config + */ + function __BaseERC4626Task_init_unchained(BaseERC4626Config memory config) internal onlyInitializing { + _setConnector(config.connector); + } + + /** + * @dev Sets the task connector + * @param newConnector Address of the new connector to be set + */ + function setConnector(address newConnector) external override authP(authParams(newConnector)) { + _setConnector(newConnector); + } + + /** + * @dev Before base ERC4626 task hook + */ + function _beforeBaseERC4626Task(address token, uint256 amount) internal virtual { + _beforeTask(token, amount); + if (token == address(0)) revert TaskTokenZero(); + if (amount == 0) revert TaskAmountZero(); + } + + /** + * @dev After base ERC4626 task hook + */ + function _afterBaseERC4626Task(address tokenIn, uint256 amountIn, address tokenOut, uint256 amountOut) + internal + virtual + { + _increaseBalanceConnector(tokenOut, amountOut); + _afterTask(tokenIn, amountIn); + } + + /** + * @dev Sets the task connector + * @param newConnector New connector to be set + */ + function _setConnector(address newConnector) internal { + if (newConnector == address(0)) revert TaskConnectorZero(); + connector = newConnector; + emit ConnectorSet(newConnector); + } +} diff --git a/packages/tasks/contracts/liquidity/erc4626/ERC4626Exiter.sol b/packages/tasks/contracts/liquidity/erc4626/ERC4626Exiter.sol new file mode 100644 index 00000000..860f93ff --- /dev/null +++ b/packages/tasks/contracts/liquidity/erc4626/ERC4626Exiter.sol @@ -0,0 +1,102 @@ +// 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-connectors/contracts/interfaces/erc4626/IERC4626Connector.sol'; + +import './BaseERC4626Task.sol'; +import '../../interfaces/liquidity/erc4626/IERC4626Exiter.sol'; + +/** + * @title ERC4626 exiter + * @dev Task that extends the base ERC4626 task to exit an ERC4626 vault + */ +contract ERC4626Exiter is IERC4626Exiter, BaseERC4626Task { + // Execution type for relayers + bytes32 public constant override EXECUTION_TYPE = keccak256('ERC4626_EXITER'); + + /** + * @dev ERC4626 exit config. Only used in the initializer. + */ + struct ERC4626ExitConfig { + BaseERC4626Config baseERC4626Config; + } + + /** + * @dev Initializes a ERC4626 exiter + * @param config ERC4626 exit config + */ + function initialize(ERC4626ExitConfig memory config) external virtual initializer { + __ERC4626Exiter_init(config); + } + + /** + * @dev Initializes the ERC4626 exiter. It does call upper contracts initializers. + * @param config ERC4626 exit config + */ + function __ERC4626Exiter_init(ERC4626ExitConfig memory config) internal onlyInitializing { + __BaseERC4626Task_init(config.baseERC4626Config); + __ERC4626Exiter_init_unchained(config); + } + + /** + * @dev Initializes the ERC4626 exiter. It does not call upper contracts initializers. + * @param config ERC4626 exit config + */ + function __ERC4626Exiter_init_unchained(ERC4626ExitConfig memory config) internal onlyInitializing { + // solhint-disable-previous-line no-empty-blocks + } + + /** + * @dev Executes the ERC4626 exiter task. Note that the ERC4626 is also the token. + * @param erc4626 Address of the ERC4626 to be exited + * @param amount Amount of shares to be exited with + * @param minAmountOut Minimum amount of assets willing to receive + */ + function call(address erc4626, uint256 amount, uint256 minAmountOut) + external + override + authP(authParams(erc4626, amount, minAmountOut)) + { + if (amount == 0) amount = getTaskAmount(erc4626); + _beforeERC4626Exiter(erc4626, amount); + bytes memory connectorData = abi.encodeWithSelector( + IERC4626Connector.exit.selector, + erc4626, + amount, + minAmountOut + ); + bytes memory result = ISmartVault(smartVault).execute(connector, connectorData); + (address tokenOut, uint256 amountOut) = abi.decode(result, (address, uint256)); + _afterERC4626Exiter(erc4626, amount, tokenOut, amountOut); + } + + /** + * @dev Before ERC4626 exiter hook + */ + function _beforeERC4626Exiter(address token, uint256 amount) internal virtual { + _beforeBaseERC4626Task(token, amount); + } + + /** + * @dev After ERC4626 exiter hook + */ + function _afterERC4626Exiter(address tokenIn, uint256 amountIn, address tokenOut, uint256 amountOut) + internal + virtual + { + _afterBaseERC4626Task(tokenIn, amountIn, tokenOut, amountOut); + } +} diff --git a/packages/tasks/contracts/liquidity/erc4626/ERC4626Joiner.sol b/packages/tasks/contracts/liquidity/erc4626/ERC4626Joiner.sol new file mode 100644 index 00000000..071846e8 --- /dev/null +++ b/packages/tasks/contracts/liquidity/erc4626/ERC4626Joiner.sol @@ -0,0 +1,105 @@ +// 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-connectors/contracts/interfaces/erc4626/IERC4626Connector.sol'; + +import './BaseERC4626Task.sol'; +import '../../interfaces/liquidity/erc4626/IERC4626Joiner.sol'; + +/** + * @title ERC4626 joiner + * @dev Task that extends the base ERC4626 task to join an ERC4626 vault + */ +contract ERC4626Joiner is IERC4626Joiner, BaseERC4626Task { + // Execution type for relayers + bytes32 public constant override EXECUTION_TYPE = keccak256('ERC4626_JOINER'); + + /** + * @dev ERC4626 join config. Only used in the initializer. + */ + struct ERC4626JoinConfig { + BaseERC4626Config baseERC4626Config; + } + + /** + * @dev Initializes a ERC4626 joiner + * @param config ERC4626 join config + */ + function initialize(ERC4626JoinConfig memory config) external virtual initializer { + __ERC4626Joiner_init(config); + } + + /** + * @dev Initializes the ERC4626 joiner. It does call upper contracts initializers. + * @param config ERC4626 join config + */ + function __ERC4626Joiner_init(ERC4626JoinConfig memory config) internal onlyInitializing { + __BaseERC4626Task_init(config.baseERC4626Config); + __ERC4626Joiner_init_unchained(config); + } + + /** + * @dev Initializes the ERC4626 joiner. It does not call upper contracts initializers. + * @param config ERC4626 join config + */ + function __ERC4626Joiner_init_unchained(ERC4626JoinConfig memory config) internal onlyInitializing { + // solhint-disable-previous-line no-empty-blocks + } + + /** + * @dev Executes the ERC4626 joiner task + * @param token Address of the token to be joined with + * @param amount Amount of assets to be joined with + * @param erc4626 Address of the ERC4626 to be joined + * @param minAmountOut Minimum amount of shares willing to receive + */ + function call(address token, uint256 amount, address erc4626, uint256 minAmountOut) + external + override + authP(authParams(token, amount, erc4626, minAmountOut)) + { + if (amount == 0) amount = getTaskAmount(token); + _beforeERC4626Joiner(token, amount, erc4626); + bytes memory connectorData = abi.encodeWithSelector( + IERC4626Connector.join.selector, + erc4626, + token, + amount, + minAmountOut + ); + bytes memory result = ISmartVault(smartVault).execute(connector, connectorData); + (address tokenOut, uint256 amountOut) = abi.decode(result, (address, uint256)); + _afterERC4626Joiner(token, amount, tokenOut, amountOut); + } + + /** + * @dev Before ERC4626 joiner hook + */ + function _beforeERC4626Joiner(address token, uint256 amount, address erc4626) internal virtual { + _beforeBaseERC4626Task(token, amount); + if (erc4626 == address(0)) revert TaskERC4626Zero(); + } + + /** + * @dev After ERC4626 joiner hook + */ + function _afterERC4626Joiner(address tokenIn, uint256 amountIn, address tokenOut, uint256 amountOut) + internal + virtual + { + _afterBaseERC4626Task(tokenIn, amountIn, tokenOut, amountOut); + } +} diff --git a/packages/tasks/contracts/test/liquidity/ERC4626ConnectorMock.sol b/packages/tasks/contracts/test/liquidity/ERC4626ConnectorMock.sol new file mode 100644 index 00000000..bc519970 --- /dev/null +++ b/packages/tasks/contracts/test/liquidity/ERC4626ConnectorMock.sol @@ -0,0 +1,40 @@ +// 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; + +contract ERC4626ConnectorMock { + address public immutable tokenOut; + + event LogJoin(address erc4626, address token, uint256 amount, uint256 minAmountOut); + + event LogExit(address erc4626, uint256 amount, uint256 minAmountOut); + + constructor(address _tokenOut) { + tokenOut = _tokenOut; + } + + function join(address erc4626, address token, uint256 assets, uint256 minSharesOut) + external + returns (address, uint256) + { + emit LogJoin(erc4626, token, assets, minSharesOut); + return (erc4626, minSharesOut); + } + + function exit(address erc4626, uint256 shares, uint256 minAssetsOut) external returns (address, uint256) { + emit LogExit(erc4626, shares, minAssetsOut); + return (tokenOut, minAssetsOut); + } +} diff --git a/packages/tasks/test/liquidity/erc4626/BaseERC4626Task.behavior.ts b/packages/tasks/test/liquidity/erc4626/BaseERC4626Task.behavior.ts new file mode 100644 index 00000000..c0c77e74 --- /dev/null +++ b/packages/tasks/test/liquidity/erc4626/BaseERC4626Task.behavior.ts @@ -0,0 +1,57 @@ +import { assertEvent, deployTokenMock, ZERO_ADDRESS } from '@mimic-fi/v3-helpers' +import { expect } from 'chai' +import { Contract } from 'ethers' +import { ethers } from 'hardhat' + +export function itBehavesLikeBaseERC4626Task(executionType: string): void { + describe('execution type', () => { + it('defines it correctly', async function () { + const expectedType = ethers.utils.solidityKeccak256(['string'], [executionType]) + expect(await this.task.EXECUTION_TYPE()).to.be.equal(expectedType) + }) + }) + + describe('setConnector', () => { + context('when the sender is authorized', () => { + beforeEach('authorize sender', async function () { + const setConnectorRole = this.task.interface.getSighash('setConnector') + await this.authorizer.connect(this.owner).authorize(this.owner.address, this.task.address, setConnectorRole, []) + this.task = this.task.connect(this.owner) + }) + + context('when the new connector is not zero', () => { + let connector: Contract + + beforeEach('deploy connector', async function () { + connector = await deployTokenMock('TKN') + }) + + it('sets the token out', async function () { + await this.task.setConnector(connector.address) + + expect(await this.task.connector()).to.be.equal(connector.address) + }) + + it('emits an event', async function () { + const tx = await this.task.setConnector(connector.address) + + await assertEvent(tx, 'ConnectorSet', { connector }) + }) + }) + + context('when the new connector is zero', () => { + const connector = ZERO_ADDRESS + + it('reverts', async function () { + await expect(this.task.setConnector(connector)).to.be.revertedWith('TaskConnectorZero') + }) + }) + }) + + context('when the sender is not authorized', () => { + it('reverts', async function () { + await expect(this.task.setConnector(ZERO_ADDRESS)).to.be.revertedWith('AuthSenderNotAllowed') + }) + }) + }) +} diff --git a/packages/tasks/test/liquidity/erc4626/ERC4626Exiter.test.ts b/packages/tasks/test/liquidity/erc4626/ERC4626Exiter.test.ts new file mode 100644 index 00000000..d4b202fc --- /dev/null +++ b/packages/tasks/test/liquidity/erc4626/ERC4626Exiter.test.ts @@ -0,0 +1,231 @@ +import { OP } from '@mimic-fi/v3-authorizer' +import { + assertEvent, + assertIndirectEvent, + assertNoEvent, + BigNumberish, + deploy, + deployProxy, + deployTokenMock, + fp, + getSigners, + ONES_ADDRESS, + 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 '../../../' +import { itBehavesLikeBaseERC4626Task } from './BaseERC4626Task.behavior' + +describe('ERC4626Exiter', () => { + let task: Contract + let smartVault: Contract, authorizer: Contract, connector: Contract, owner: SignerWithAddress + + const tokenOut = ONES_ADDRESS + + before('setup', async () => { + // eslint-disable-next-line prettier/prettier + ([, owner] = await getSigners()) + ;({ authorizer, smartVault } = await deployEnvironment(owner)) + }) + + before('deploy connector', async () => { + connector = await deploy('ERC4626ConnectorMock', [tokenOut]) + 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( + 'ERC4626Exiter', + [], + [ + { + baseERC4626Config: { + connector: connector.address, + taskConfig: buildEmptyTaskConfig(owner, smartVault), + }, + }, + ] + ) + }) + + describe('ERC4626', () => { + beforeEach('set params', async function () { + this.owner = owner + this.task = task + this.authorizer = authorizer + }) + + itBehavesLikeBaseERC4626Task('ERC4626_EXITER') + }) + + 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 is not zero', () => { + let erc4626: Contract + + beforeEach('deploy tokens', async () => { + erc4626 = await deployTokenMock('ERC4626') // token in + }) + + context('when the amount is not zero', () => { + const amount = fp(10) + const shareValue = fp(2.5) + const minAmountOut = amount.mul(shareValue) + + beforeEach('fund smart vault', async () => { + await erc4626.mint(smartVault.address, amount) + }) + + context('when the threshold has passed', () => { + const threshold = amount + + beforeEach('set token threshold', async () => { + const setDefaultTokenThresholdRole = task.interface.getSighash('setDefaultTokenThreshold') + await authorizer.connect(owner).authorize(owner.address, task.address, setDefaultTokenThresholdRole, []) + await task.connect(owner).setDefaultTokenThreshold(erc4626.address, threshold, 0) + }) + + const itExecutesTheTaskProperly = (requestedAmount: BigNumberish) => { + it('executes the expected connector', async () => { + const tx = await task.call(erc4626.address, requestedAmount, minAmountOut) + + const connectorData = connector.interface.encodeFunctionData('exit', [ + erc4626.address, + amount, + minAmountOut, + ]) + + await assertIndirectEvent(tx, smartVault.interface, 'Executed', { connector, data: connectorData }) + + await assertIndirectEvent(tx, connector.interface, 'LogExit', { + erc4626, + amount, + minAmountOut, + }) + }) + + it('emits an Executed event', async () => { + const tx = await task.call(erc4626.address, requestedAmount, minAmountOut) + await assertEvent(tx, 'Executed') + }) + } + + context('without balance connectors', () => { + const requestedAmount = amount + + itExecutesTheTaskProperly(requestedAmount) + + it('does not update any balance connectors', async () => { + const tx = await task.call(erc4626.address, requestedAmount, minAmountOut) + + 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, erc4626.address, amount, true) + }) + + itExecutesTheTaskProperly(requestedAmount) + + it('updates the balance connectors properly', async () => { + const tx = await task.call(erc4626.address, requestedAmount, minAmountOut) + + await assertIndirectEvent(tx, smartVault.interface, 'BalanceConnectorUpdated', { + id: prevConnectorId, + token: erc4626.address, + amount, + added: false, + }) + + await assertIndirectEvent(tx, smartVault.interface, 'BalanceConnectorUpdated', { + id: nextConnectorId, + token: tokenOut, + amount: minAmountOut, + added: true, + }) + }) + }) + }) + + context('when the threshold has not passed', () => { + const threshold = amount.add(1) + + beforeEach('set token threshold', async () => { + const setDefaultTokenThresholdRole = task.interface.getSighash('setDefaultTokenThreshold') + await authorizer.connect(owner).authorize(owner.address, task.address, setDefaultTokenThresholdRole, []) + await task.connect(owner).setDefaultTokenThreshold(erc4626.address, threshold, 0) + }) + + it('reverts', async () => { + await expect(task.call(erc4626.address, amount, minAmountOut)).to.be.revertedWith( + 'TaskTokenThresholdNotMet' + ) + }) + }) + }) + + context('when the amount is zero', () => { + const amount = 0 + + it('reverts', async () => { + await expect(task.call(erc4626.address, amount, 0)).to.be.revertedWith('TaskAmountZero') + }) + }) + }) + + context('when the token is zero', () => { + const erc4626 = ZERO_ADDRESS + + it('reverts', async () => { + await expect(task.call(erc4626, 0, 0)).to.be.revertedWith('TaskTokenZero') + }) + }) + }) + + context('when the sender is not authorized', () => { + it('reverts', async () => { + await expect(task.call(ZERO_ADDRESS, 0, 0)).to.be.revertedWith('AuthSenderNotAllowed') + }) + }) + }) +}) diff --git a/packages/tasks/test/liquidity/erc4626/ERC4626Joiner.test.ts b/packages/tasks/test/liquidity/erc4626/ERC4626Joiner.test.ts new file mode 100644 index 00000000..1e34e0ed --- /dev/null +++ b/packages/tasks/test/liquidity/erc4626/ERC4626Joiner.test.ts @@ -0,0 +1,242 @@ +import { OP } from '@mimic-fi/v3-authorizer' +import { + assertEvent, + assertIndirectEvent, + assertNoEvent, + BigNumberish, + deploy, + deployProxy, + deployTokenMock, + fp, + getSigners, + ONES_ADDRESS, + 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 '../../../' +import { itBehavesLikeBaseERC4626Task } from './BaseERC4626Task.behavior' + +describe('ERC4626Joiner', () => { + 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('ERC4626ConnectorMock', [ONES_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( + 'ERC4626Joiner', + [], + [ + { + baseERC4626Config: { + connector: connector.address, + taskConfig: buildEmptyTaskConfig(owner, smartVault), + }, + }, + ] + ) + }) + + describe('ERC4626', () => { + beforeEach('set params', async function () { + this.owner = owner + this.task = task + this.authorizer = authorizer + }) + + itBehavesLikeBaseERC4626Task('ERC4626_JOINER') + }) + + 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 is not zero', () => { + let token: Contract, erc4626: Contract + + beforeEach('deploy tokens', async () => { + token = await deployTokenMock('WETH') + erc4626 = await deployTokenMock('ERC4626') // token out + }) + + context('when the amount is not zero', () => { + const amount = fp(10) + const shareValue = fp(2.5) + const minAmountOut = amount.div(shareValue) + + beforeEach('fund smart vault', async () => { + await token.mint(smartVault.address, amount) + }) + + context('when the ERC4626 is not zero', () => { + context('when the threshold has passed', () => { + const threshold = amount + + beforeEach('set token threshold', async () => { + const setDefaultTokenThresholdRole = task.interface.getSighash('setDefaultTokenThreshold') + await authorizer.connect(owner).authorize(owner.address, task.address, setDefaultTokenThresholdRole, []) + await task.connect(owner).setDefaultTokenThreshold(token.address, threshold, 0) + }) + + const itExecutesTheTaskProperly = (requestedAmount: BigNumberish) => { + it('executes the expected connector', async () => { + const tx = await task.call(token.address, requestedAmount, erc4626.address, minAmountOut) + + const connectorData = connector.interface.encodeFunctionData('join', [ + erc4626.address, + token.address, + amount, + minAmountOut, + ]) + await assertIndirectEvent(tx, smartVault.interface, 'Executed', { connector, data: connectorData }) + await assertIndirectEvent(tx, connector.interface, 'LogJoin', { + erc4626, + token, + amount, + minAmountOut, + }) + }) + + it('emits an Executed event', async () => { + const tx = await task.call(token.address, requestedAmount, erc4626.address, minAmountOut) + await assertEvent(tx, 'Executed') + }) + } + + context('without balance connectors', () => { + const requestedAmount = amount + + itExecutesTheTaskProperly(requestedAmount) + + it('does not update any balance connectors', async () => { + const tx = await task.call(token.address, requestedAmount, erc4626.address, minAmountOut) + + 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, token.address, amount, true) + }) + + itExecutesTheTaskProperly(requestedAmount) + + it('updates the balance connectors properly', async () => { + const tx = await task.call(token.address, amount, erc4626.address, minAmountOut) + + await assertIndirectEvent(tx, smartVault.interface, 'BalanceConnectorUpdated', { + id: prevConnectorId, + token, + amount, + added: false, + }) + + await assertIndirectEvent(tx, smartVault.interface, 'BalanceConnectorUpdated', { + id: nextConnectorId, + token: erc4626.address, + amount: minAmountOut, + added: true, + }) + }) + }) + }) + + context('when the threshold has not passed', () => { + const threshold = amount.add(1) + + beforeEach('set token threshold', async () => { + const setDefaultTokenThresholdRole = task.interface.getSighash('setDefaultTokenThreshold') + await authorizer.connect(owner).authorize(owner.address, task.address, setDefaultTokenThresholdRole, []) + await task.connect(owner).setDefaultTokenThreshold(token.address, threshold, 0) + }) + + it('reverts', async () => { + await expect(task.call(token.address, amount, erc4626.address, minAmountOut)).to.be.revertedWith( + 'TaskTokenThresholdNotMet' + ) + }) + }) + }) + + context('when the ERC4626 is zero', () => { + const erc4626 = ZERO_ADDRESS + + it('reverts', async () => { + await expect(task.call(token.address, amount, erc4626, minAmountOut)).to.be.revertedWith( + 'TaskERC4626Zero' + ) + }) + }) + }) + + context('when the amount is zero', () => { + const amount = 0 + + it('reverts', async () => { + await expect(task.call(token.address, amount, ZERO_ADDRESS, 0)).to.be.revertedWith('TaskAmountZero') + }) + }) + }) + + context('when the token is zero', () => { + const token = ZERO_ADDRESS + + it('reverts', async () => { + await expect(task.call(token, 0, ZERO_ADDRESS, 0)).to.be.revertedWith('TaskTokenZero') + }) + }) + }) + + context('when the sender is not authorized', () => { + it('reverts', async () => { + await expect(task.call(ZERO_ADDRESS, 0, ZERO_ADDRESS, 0)).to.be.revertedWith('AuthSenderNotAllowed') + }) + }) + }) +})