diff --git a/packages/connectors/package.json b/packages/connectors/package.json index 9687dc66..653bd776 100644 --- a/packages/connectors/package.json +++ b/packages/connectors/package.json @@ -1,6 +1,6 @@ { "name": "@mimic-fi/v3-connectors", - "version": "0.2.3", + "version": "0.2.4", "license": "GPL-3.0", "files": [ "artifacts/contracts/**/*", diff --git a/packages/tasks/contracts/bridge/SocketBridger.sol b/packages/tasks/contracts/bridge/SocketBridger.sol new file mode 100644 index 00000000..6410c4cb --- /dev/null +++ b/packages/tasks/contracts/bridge/SocketBridger.sol @@ -0,0 +1,92 @@ +// 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/math/FixedPoint.sol'; +import '@mimic-fi/v3-connectors/contracts/interfaces/socket/ISocketConnector.sol'; + +import './BaseBridgeTask.sol'; +import '../interfaces/bridge/ISocketBridger.sol'; + +/** + * @title Socket bridger + * @dev Task that extends the base bridge task to use Socket + */ +contract SocketBridger is ISocketBridger, BaseBridgeTask { + using FixedPoint for uint256; + + // Execution type for relayers + bytes32 public constant override EXECUTION_TYPE = keccak256('SOCKET_BRIDGER'); + + /** + * @dev Socket bridge config. Only used in the initializer. + */ + struct SocketBridgeConfig { + BaseBridgeConfig baseBridgeConfig; + } + + /** + * @dev Initializes the Socket bridger + * @param config Socket bridge config + */ + function initialize(SocketBridgeConfig memory config) external virtual initializer { + __SocketBridger_init(config); + } + + /** + * @dev Initializes the Socket bridger. It does call upper contracts initializers. + * @param config Socket bridge config + */ + function __SocketBridger_init(SocketBridgeConfig memory config) internal onlyInitializing { + __BaseBridgeTask_init(config.baseBridgeConfig); + __SocketBridger_init_unchained(config); + } + + /** + * @dev Initializes the Socket bridger. It does not call upper contracts initializers. + * @param config Socket bridge config + */ + function __SocketBridger_init_unchained(SocketBridgeConfig memory config) internal onlyInitializing { + // solhint-disable-previous-line no-empty-blocks + } + + /** + * @dev Execute Socket bridger + */ + function call(address token, uint256 amount, bytes memory data) external override authP(authParams(token, amount)) { + if (amount == 0) amount = getTaskAmount(token); + _beforeSocketBridger(token, amount); + + bytes memory connectorData = abi.encodeWithSelector(ISocketConnector.execute.selector, token, amount, data); + ISmartVault(smartVault).execute(connector, connectorData); + _afterSocketBridger(token, amount); + } + + /** + * @dev Before Socket bridger hook + */ + function _beforeSocketBridger(address token, uint256 amount) internal virtual { + // Socket does not support specifying slippage nor fee + _beforeBaseBridgeTask(token, amount, 0, 0); + } + + /** + * @dev After Socket bridger task hook + */ + function _afterSocketBridger(address token, uint256 amount) internal virtual { + // Socket does not support specifying slippage nor fee + _afterBaseBridgeTask(token, amount, 0, 0); + } +} diff --git a/packages/tasks/contracts/interfaces/bridge/ISocketBridger.sol b/packages/tasks/contracts/interfaces/bridge/ISocketBridger.sol new file mode 100644 index 00000000..542d3fea --- /dev/null +++ b/packages/tasks/contracts/interfaces/bridge/ISocketBridger.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 './IBaseBridgeTask.sol'; + +/** + * @dev Socket bridger task interface + */ +interface ISocketBridger is IBaseBridgeTask { + /** + * @dev Execute Socket bridger task + */ + function call(address token, uint256 amount, bytes memory data) external; +} diff --git a/packages/tasks/contracts/swap/OneInchV5Swapper.sol b/packages/tasks/contracts/swap/OneInchV5Swapper.sol index 7439eb3a..756c1e78 100644 --- a/packages/tasks/contracts/swap/OneInchV5Swapper.sol +++ b/packages/tasks/contracts/swap/OneInchV5Swapper.sol @@ -69,8 +69,8 @@ contract OneInchV5Swapper is IOneInchV5Swapper, BaseSwapTask { */ function call(address tokenIn, uint256 amountIn, uint256 slippage, bytes memory data) external - override virtual + override authP(authParams(tokenIn, amountIn, slippage)) { if (amountIn == 0) amountIn = getTaskAmount(tokenIn); diff --git a/packages/tasks/contracts/test/bridge/SocketConnectorMock.sol b/packages/tasks/contracts/test/bridge/SocketConnectorMock.sol new file mode 100644 index 00000000..c3ed80de --- /dev/null +++ b/packages/tasks/contracts/test/bridge/SocketConnectorMock.sol @@ -0,0 +1,23 @@ +// 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 SocketConnectorMock { + event LogExecute(address token, uint256 amount, bytes data); + + function execute(address token, uint256 amount, bytes memory data) external { + emit LogExecute(token, amount, data); + } +} diff --git a/packages/tasks/package.json b/packages/tasks/package.json index 3987b3a5..8f265ef4 100644 --- a/packages/tasks/package.json +++ b/packages/tasks/package.json @@ -23,7 +23,7 @@ }, "dependencies": { "@mimic-fi/v3-authorizer": "0.1.1", - "@mimic-fi/v3-connectors": "0.2.3", + "@mimic-fi/v3-connectors": "0.2.4", "@mimic-fi/v3-helpers": "0.1.9", "@mimic-fi/v3-price-oracle": "0.1.0", "@mimic-fi/v3-smart-vault": "0.1.0", diff --git a/packages/tasks/test/bridge/SocketBridger.test.ts b/packages/tasks/test/bridge/SocketBridger.test.ts new file mode 100644 index 00000000..6ef736b8 --- /dev/null +++ b/packages/tasks/test/bridge/SocketBridger.test.ts @@ -0,0 +1,272 @@ +import { OP } from '@mimic-fi/v3-authorizer' +import { + assertEvent, + assertIndirectEvent, + assertNoEvent, + BigNumberish, + deploy, + deployProxy, + deployTokenMock, + fp, + getSigners, + ZERO_ADDRESS, + ZERO_BYTES32, +} 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 '../../src/setup' +import { itBehavesLikeBaseBridgeTask } from './BaseBridgeTask.behavior' + +describe('SocketBridger', () => { + let task: Contract + let smartVault: Contract, authorizer: Contract, connector: Contract + let 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('SocketConnectorMock') + 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( + 'SocketBridger', + [], + [ + { + baseBridgeConfig: { + connector: connector.address, + recipient: smartVault.address, + destinationChain: 0, + maxSlippage: fp(0.01), + maxFee: { + token: ZERO_ADDRESS, + amount: 0, + }, + customDestinationChains: [], + customMaxSlippages: [], + customMaxFees: [], + taskConfig: buildEmptyTaskConfig(owner, smartVault), + }, + }, + ] + ) + }) + + describe('bridger', () => { + beforeEach('set params', async function () { + this.owner = owner + this.task = task + this.authorizer = authorizer + }) + + itBehavesLikeBaseBridgeTask('SOCKET_BRIDGER') + }) + + describe('call', () => { + const socketData = '0xabcdef' + + 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 the address zero', () => { + let token: Contract + + beforeEach('deploy token', async () => { + token = await deployTokenMock('TKN') + }) + + context('when the amount is not zero', () => { + const amount = fp(100) + + context('when the destination chain was set', () => { + const chainId = 1 + + beforeEach('set destination chain ID', async () => { + const setDefaultDestinationChainRole = task.interface.getSighash('setDefaultDestinationChain') + await authorizer.connect(owner).authorize(owner.address, task.address, setDefaultDestinationChainRole, []) + await task.connect(owner).setDefaultDestinationChain(chainId) + }) + + context('when the given token is allowed', () => { + context('when the current balance passes the threshold', () => { + const threshold = amount + + beforeEach('set 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) + }) + + beforeEach('fund smart vault', async () => { + await token.mint(smartVault.address, amount) + }) + + const itExecutesTheTaskProperly = (requestedAmount: BigNumberish) => { + it('executes the expected connector', async () => { + const tx = await task.call(token.address, requestedAmount, socketData) + + const connectorData = connector.interface.encodeFunctionData('execute', [ + token.address, + amount, + socketData, + ]) + + await assertIndirectEvent(tx, smartVault.interface, 'Executed', { + connector, + data: connectorData, + }) + + await assertIndirectEvent(tx, connector.interface, 'LogExecute', { + token, + amount, + data: socketData, + }) + }) + + it('emits an Executed event', async () => { + const tx = await task.call(token.address, requestedAmount, socketData) + + 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, socketData) + + await assertNoEvent(tx, 'BalanceConnectorUpdated') + }) + }) + + context('with balance connectors', () => { + const requestedAmount = 0 + const prevConnectorId = '0x0000000000000000000000000000000000000000000000000000000000000001' + + 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, ZERO_BYTES32) + }) + + 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 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, socketData) + + await assertIndirectEvent(tx, smartVault.interface, 'BalanceConnectorUpdated', { + id: prevConnectorId, + token, + amount: amount, + added: false, + }) + }) + }) + }) + + context('when the current balance does not pass the threshold', () => { + const threshold = amount.add(1) + + beforeEach('set 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, socketData)).to.be.revertedWith( + 'TaskTokenThresholdNotMet' + ) + }) + }) + }) + + context('when the given token is not allowed', () => { + beforeEach('deny token', async () => { + const setTokensAcceptanceListRole = task.interface.getSighash('setTokensAcceptanceList') + await authorizer.connect(owner).authorize(owner.address, task.address, setTokensAcceptanceListRole, []) + await task.connect(owner).setTokensAcceptanceList([token.address], [true]) + }) + + it('reverts', async () => { + await expect(task.call(token.address, amount, socketData)).to.be.revertedWith('TaskTokenNotAllowed') + }) + }) + }) + + context('when the destination chain was not set', () => { + it('reverts', async () => { + await expect(task.call(token.address, amount, socketData)).to.be.revertedWith( + 'TaskDestinationChainNotSet' + ) + }) + }) + }) + + context('when the amount is zero', () => { + const amount = 0 + + it('reverts', async () => { + await expect(task.call(token.address, amount, socketData)).to.be.revertedWith('TaskAmountZero') + }) + }) + }) + + context('when the token is the address zero', () => { + const token = ZERO_ADDRESS + + it('reverts', async () => { + await expect(task.call(token, 0, socketData)).to.be.revertedWith('TaskTokenZero') + }) + }) + }) + + context('when the sender is not authorized', () => { + it('reverts', async () => { + await expect(task.call(ZERO_ADDRESS, 0, socketData)).to.be.revertedWith('AuthSenderNotAllowed') + }) + }) + }) +})