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