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