diff --git a/packages/connectors/contracts/interfaces/socket/ISocketConnector.sol b/packages/connectors/contracts/interfaces/socket/ISocketConnector.sol new file mode 100644 index 00000000..ba04445c --- /dev/null +++ b/packages/connectors/contracts/interfaces/socket/ISocketConnector.sol @@ -0,0 +1,38 @@ +// 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; + +/** + * @title Socket connector interface + */ +interface ISocketConnector { + /** + * @dev The post token balance is lower than the previous token balance minus the amount bridged + */ + error SocketBridgeBadPostTokenBalance(uint256 postBalance, uint256 preBalance, uint256 amount); + + /** + * @dev Tells the reference to the Socket gateway of the source chain + */ + function socketGateway() external view returns (address); + + /** + * @dev Executes a bridge of assets using Socket + * @param token Address of the token to be bridged + * @param amount Amount of tokens to be bridged + * @param data Data to be sent to the socket gateway + */ + function execute(address token, uint256 amount, bytes memory data) external; +} diff --git a/packages/connectors/contracts/socket/SocketConnector.sol b/packages/connectors/contracts/socket/SocketConnector.sol new file mode 100644 index 00000000..e9c512d2 --- /dev/null +++ b/packages/connectors/contracts/socket/SocketConnector.sol @@ -0,0 +1,54 @@ +// 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 '@openzeppelin/contracts/utils/Address.sol'; + +import '@mimic-fi/v3-helpers/contracts/utils/ERC20Helpers.sol'; + +import '../interfaces/socket/ISocketConnector.sol'; + +/** + * @title SocketConnector + * @dev Interfaces with Socket to bridge tokens + */ +contract SocketConnector is ISocketConnector { + // Reference to the Socket gateway of the source chain + address public immutable override socketGateway; + + /** + * @dev Creates a new Socket connector + * @param _socketGateway Address of the Socket gateway for the source chain + */ + constructor(address _socketGateway) { + socketGateway = _socketGateway; + } + + /** + * @dev Executes a bridge of assets using Socket + * @param token Address of the token to be bridged + * @param amount Amount of tokens to be bridged + * @param data Data to be sent to the Socket gateway + */ + function execute(address token, uint256 amount, bytes memory data) external override { + uint256 preBalance = IERC20(token).balanceOf(address(this)); + ERC20Helpers.approve(token, socketGateway, amount); + Address.functionCall(socketGateway, data, 'SOCKET_BRIDGE_FAILED'); + + uint256 postBalance = IERC20(token).balanceOf(address(this)); + bool isPostBalanceUnexpected = postBalance < preBalance - amount; + if (isPostBalanceUnexpected) revert SocketBridgeBadPostTokenBalance(postBalance, preBalance, amount); + } +} diff --git a/packages/connectors/package.json b/packages/connectors/package.json index 0cb57dff..9687dc66 100644 --- a/packages/connectors/package.json +++ b/packages/connectors/package.json @@ -21,7 +21,7 @@ "test:arbitrum": "yarn test --fork arbitrum --block-number 212259071 --chain-id 42161", "test:gnosis": "yarn test --fork gnosis --block-number 28580764 --chain-id 100", "test:avalanche": "yarn test --fork avalanche --block-number 31333905 --chain-id 43114", - "test:bsc": "yarn test --fork bsc --block-number 41410900 --chain-id 56", + "test:bsc": "yarn test --fork bsc --block-number 42144988 --chain-id 56", "test:fantom": "yarn test --fork fantom --block-number 61485606 --chain-id 250", "test:zkevm": "yarn test --fork zkevm --block-number 9014946 --chain-id 1101", "test:base": "yarn test --fork base --block-number 18341220 --chain-id 8453", diff --git a/packages/connectors/src/socket.ts b/packages/connectors/src/socket.ts new file mode 100644 index 00000000..1b0a45b7 --- /dev/null +++ b/packages/connectors/src/socket.ts @@ -0,0 +1,73 @@ +import axios, { AxiosError } from 'axios' +import { BigNumber, Contract } from 'ethers' + +const SOCKET_URL = 'https://api.socket.tech/v2' +const SOCKET_API_KEY = '72a5b4b0-e727-48be-8aa1-5da9d62fe635' + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export type QuoteResponse = { data: { result: { routes: any[] } } } +export type TransactionDataResponse = { data: { result: { txData: string } } } + +export async function getSocketBridgeData( + sender: Contract, + fromChainId: number, + fromToken: Contract, + fromAmount: BigNumber, + toChainId: number, + toToken: Contract, + slippage: number +): Promise { + try { + const quote = await getQuote(sender, fromChainId, fromToken, fromAmount, toChainId, toToken, slippage) + const transaction = await getTransactionData(quote.data.result.routes[0]) + return transaction.data.result.txData + } catch (error) { + if (error instanceof AxiosError) throw Error(error.toString() + ' - ' + error.response?.data?.description) + else throw error + } +} + +async function getQuote( + sender: Contract, + fromChainId: number, + fromToken: Contract, + fromAmount: BigNumber, + toChainId: number, + toToken: Contract, + slippage: number +): Promise { + return axios.get(`${SOCKET_URL}/quote`, { + headers: { + 'API-KEY': SOCKET_API_KEY, + Accept: 'application/json', + }, + params: { + userAddress: sender.address, + fromChainId: fromChainId, + fromTokenAddress: fromToken.address, + fromAmount: fromAmount.toString(), + toChainId: toChainId, + toTokenAddress: toToken.address, + defaultBridgeSlippage: slippage < 1 ? slippage * 100 : slippage, + singleTxOnly: true, + uniqueRoutesPerBridge: true, + sort: 'output', + includeDexes: ['oneinch', 'rainbow'], + includeBridges: ['cctp', 'celer', 'connext', 'hop', 'stargate'], + }, + }) +} + +async function getTransactionData(route: any): Promise { + return axios.post( + `${SOCKET_URL}/build-tx`, + { route }, + { + headers: { + 'API-KEY': SOCKET_API_KEY, + Accept: 'application/json', + }, + } + ) +} diff --git a/packages/connectors/test/connext/ConnextConnector.behavior.ts b/packages/connectors/test/connext/ConnextConnector.behavior.ts index ab4bd6d1..e38f1469 100644 --- a/packages/connectors/test/connext/ConnextConnector.behavior.ts +++ b/packages/connectors/test/connext/ConnextConnector.behavior.ts @@ -20,7 +20,7 @@ export function itBehavesLikeConnextConnector( }) context('when the recipient is not the zero address', async () => { - const slippage = 0.05 + const slippage = 0.15 const relayerFee = amount.div(10) let minAmountOut: BigNumber, amountAfterFees: BigNumber diff --git a/packages/connectors/test/connext/ConnextConnector.bsc.ts b/packages/connectors/test/connext/ConnextConnector.bsc.ts index ecaf9a54..7805cd9c 100644 --- a/packages/connectors/test/connext/ConnextConnector.bsc.ts +++ b/packages/connectors/test/connext/ConnextConnector.bsc.ts @@ -1,11 +1,10 @@ -import { deploy, fp, toUSDC } from '@mimic-fi/v3-helpers' +import { deploy, toUSDC } from '@mimic-fi/v3-helpers' import { itBehavesLikeConnextConnector } from './ConnextConnector.behavior' /* eslint-disable no-secrets/no-secrets */ const USDC = '0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d' -const WETH = '0x2170Ed0880ac9A755fd29B2688956BD959F933F8' const WHALE = '0x8894e0a0c962cb723c1976a4421c95949be2d4e3' const CONNEXT = '0xCd401c10afa37d641d2F594852DA94C700e4F2CE' @@ -20,8 +19,4 @@ describe('ConnextConnector', () => { context('USDC', () => { itBehavesLikeConnextConnector(SOURCE_CHAIN_ID, USDC, toUSDC(300), CONNEXT, WHALE) }) - - context('WETH', () => { - itBehavesLikeConnextConnector(SOURCE_CHAIN_ID, WETH, fp(0.5), CONNEXT, WHALE) - }) }) diff --git a/packages/connectors/test/connext/ConnextConnector.optimism.ts b/packages/connectors/test/connext/ConnextConnector.optimism.ts index 9031e1d9..3b5ffe6e 100644 --- a/packages/connectors/test/connext/ConnextConnector.optimism.ts +++ b/packages/connectors/test/connext/ConnextConnector.optimism.ts @@ -1,11 +1,10 @@ -import { deploy, fp, toUSDC } from '@mimic-fi/v3-helpers' +import { deploy, toUSDC } from '@mimic-fi/v3-helpers' import { itBehavesLikeConnextConnector } from './ConnextConnector.behavior' /* eslint-disable no-secrets/no-secrets */ const USDC = '0x7F5c764cBc14f9669B88837ca1490cCa17c31607' -const WETH = '0x4200000000000000000000000000000000000006' const WHALE = '0x85149247691df622eaf1a8bd0cafd40bc45154a9' const CONNEXT = '0x8f7492DE823025b4CfaAB1D34c58963F2af5DEDA' @@ -20,8 +19,4 @@ describe('ConnextConnector', () => { context('USDC', () => { itBehavesLikeConnextConnector(SOURCE_CHAIN_ID, USDC, toUSDC(300), CONNEXT, WHALE) }) - - context('WETH', () => { - itBehavesLikeConnextConnector(SOURCE_CHAIN_ID, WETH, fp(2), CONNEXT, WHALE) - }) }) diff --git a/packages/connectors/test/helpers/socket/fixtures/56/42144988/USDC-1.json b/packages/connectors/test/helpers/socket/fixtures/56/42144988/USDC-1.json new file mode 100644 index 00000000..0c67a011 --- /dev/null +++ b/packages/connectors/test/helpers/socket/fixtures/56/42144988/USDC-1.json @@ -0,0 +1,9 @@ +{ + "fromChainId": 56, + "fromToken": "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d", + "fromAmount": "50000000000000000000000", + "toChainId": 1, + "toToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "slippage": 0.02, + "data": "0x0000000d52106ce9345fa8d86eeffdd25c389441ff96e05cf90592de8ac76a51cc950d9822d68b83fe1ad97b32cd580d0000000000000a968163f0a57b4000000000000100b8d397000068dc" +} \ No newline at end of file diff --git a/packages/connectors/test/helpers/socket/fixtures/56/42144988/USDC-10.json b/packages/connectors/test/helpers/socket/fixtures/56/42144988/USDC-10.json new file mode 100644 index 00000000..1147868a --- /dev/null +++ b/packages/connectors/test/helpers/socket/fixtures/56/42144988/USDC-10.json @@ -0,0 +1,9 @@ +{ + "fromChainId": 56, + "fromToken": "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d", + "fromAmount": "50000000000000000000000", + "toChainId": 10, + "toToken": "0x7F5c764cBc14f9669B88837ca1490cCa17c31607", + "slippage": 0.02, + "data": "0x0000000d52106ce9345fa8d86eeffdd25c389441ff96e05cf90592de8ac76a51cc950d9822d68b83fe1ad97b32cd580d0000000000000a968163f0a57b4000000000000a00b8e5120000620c" +} \ No newline at end of file diff --git a/packages/connectors/test/helpers/socket/fixtures/56/42145576/USDC-1.json b/packages/connectors/test/helpers/socket/fixtures/56/42145576/USDC-1.json new file mode 100644 index 00000000..46aceb2d --- /dev/null +++ b/packages/connectors/test/helpers/socket/fixtures/56/42145576/USDC-1.json @@ -0,0 +1,9 @@ +{ + "fromChainId": 56, + "fromToken": "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d", + "fromAmount": "50000000000000000000000", + "toChainId": 1, + "toToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "slippage": 0.02, + "data": "0x0000000d52106ce94a1bf945c2995b53a00df694fab85f453e5e1f5e8ac76a51cc950d9822d68b83fe1ad97b32cd580d0000000000000a968163f0a57b4000000000000100d2437d000068dd" +} \ No newline at end of file diff --git a/packages/connectors/test/helpers/socket/fixtures/56/42145576/USDC-10.json b/packages/connectors/test/helpers/socket/fixtures/56/42145576/USDC-10.json new file mode 100644 index 00000000..03af9246 --- /dev/null +++ b/packages/connectors/test/helpers/socket/fixtures/56/42145576/USDC-10.json @@ -0,0 +1,9 @@ +{ + "fromChainId": 56, + "fromToken": "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d", + "fromAmount": "50000000000000000000000", + "toChainId": 10, + "toToken": "0x7F5c764cBc14f9669B88837ca1490cCa17c31607", + "slippage": 0.02, + "data": "0x0000000d52106ce94a1bf945c2995b53a00df694fab85f453e5e1f5e8ac76a51cc950d9822d68b83fe1ad97b32cd580d0000000000000a968163f0a57b4000000000000a00d256670000620c" +} \ No newline at end of file diff --git a/packages/connectors/test/helpers/socket/index.ts b/packages/connectors/test/helpers/socket/index.ts new file mode 100644 index 00000000..a2eebdf8 --- /dev/null +++ b/packages/connectors/test/helpers/socket/index.ts @@ -0,0 +1,84 @@ +import { currentBlockNumber } from '@mimic-fi/v3-helpers' +import { BigNumber, Contract } from 'ethers' +import fs from 'fs' +import hre from 'hardhat' +import { HardhatNetworkConfig } from 'hardhat/types' +import path from 'path' + +import { getSocketBridgeData } from '../../../src/socket' + +type Fixture = { + fromChainId: number + fromToken: string + fromAmount: string + toChainId: number + toToken: string + slippage: number + data: string +} + +export async function loadOrGetSocketData( + sender: Contract, + fromChainId: number, + fromToken: Contract, + fromAmount: BigNumber, + toChainId: number, + toToken: Contract, + slippage: number +): Promise { + const config = hre.network.config as HardhatNetworkConfig + const blockNumber = config?.forking?.blockNumber?.toString() || (await currentBlockNumber()).toString() + + const fixture = await readFixture(fromChainId, fromToken, toChainId, toToken, blockNumber) + if (fixture) return fixture.data + + const data = await getSocketBridgeData(sender, fromChainId, fromToken, fromAmount, toChainId, toToken, slippage) + await saveFixture(fromChainId, fromToken, fromAmount, toChainId, toToken, slippage, data, blockNumber) + return data +} + +async function readFixture( + fromChainId: number, + fromToken: Contract, + toChainId: number, + toToken: Contract, + blockNumber: string +): Promise { + const bridgePath = `${await fromToken.symbol()}-${toChainId}.json` + const fixturePath = path.join(__dirname, 'fixtures', fromChainId.toString(), blockNumber, bridgePath) + if (!fs.existsSync(fixturePath)) return undefined + return JSON.parse(fs.readFileSync(fixturePath).toString()) +} + +async function saveFixture( + fromChainId: number, + fromToken: Contract, + fromAmount: BigNumber, + toChainId: number, + toToken: Contract, + slippage: number, + data: string, + blockNumber: string +): Promise { + const output = { + fromChainId: fromChainId, + fromToken: fromToken.address, + fromAmount: fromAmount.toString(), + toChainId: toChainId, + toToken: toToken.address, + slippage, + data, + } + + const fixturesPath = path.join(__dirname, 'fixtures') + if (!fs.existsSync(fixturesPath)) fs.mkdirSync(fixturesPath) + + const networkPath = path.join(fixturesPath, fromChainId.toString()) + if (!fs.existsSync(networkPath)) fs.mkdirSync(networkPath) + + const blockNumberPath = path.join(networkPath, blockNumber) + if (!fs.existsSync(blockNumberPath)) fs.mkdirSync(blockNumberPath) + + const bridgePath = path.join(blockNumberPath, `${await fromToken.symbol()}-${toChainId}.json`) + fs.writeFileSync(bridgePath, JSON.stringify(output, null, 2)) +} diff --git a/packages/connectors/test/socket/SocketConnector.behavior.ts b/packages/connectors/test/socket/SocketConnector.behavior.ts new file mode 100644 index 00000000..6380e3ce --- /dev/null +++ b/packages/connectors/test/socket/SocketConnector.behavior.ts @@ -0,0 +1,51 @@ +import { fp, impersonate, instanceAt } from '@mimic-fi/v3-helpers' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address' +import { expect } from 'chai' +import { BigNumber, Contract } from 'ethers' + +import { loadOrGetSocketData } from '../helpers/socket' + +/* eslint-disable no-secrets/no-secrets */ + +export function itBehavesLikeSocketConnector( + fromChainId: number, + fromTokenAddress: string, + fromAmount: BigNumber, + toChainId: number, + toTokenAddress: string, + whaleAddress: string +): void { + const slippage = 0.02 + let fromToken: Contract, toToken: Contract, whale: SignerWithAddress + + before('load tokens and accounts', async function () { + fromToken = await instanceAt('IERC20Metadata', fromTokenAddress) + toToken = await instanceAt('IERC20Metadata', toTokenAddress) + whale = await impersonate(whaleAddress, fp(100)) + }) + + it('should send the tokens to the socket gateway', async function () { + const previousSenderBalance = await fromToken.balanceOf(whaleAddress) + const previousConnectorBalance = await fromToken.balanceOf(this.connector.address) + + await fromToken.connect(whale).transfer(this.connector.address, fromAmount) + + const data = await loadOrGetSocketData( + this.connector, + fromChainId, + fromToken, + fromAmount, + toChainId, + toToken, + slippage + ) + + await this.connector.connect(whale).execute(fromTokenAddress, fromAmount, data) + + const currentSenderBalance = await fromToken.balanceOf(whaleAddress) + expect(currentSenderBalance).to.be.equal(previousSenderBalance.sub(fromAmount)) + + const currentConnectorBalance = await fromToken.balanceOf(this.connector.address) + expect(currentConnectorBalance).to.be.equal(previousConnectorBalance) + }) +} diff --git a/packages/connectors/test/socket/SocketConnector.bsc.ts b/packages/connectors/test/socket/SocketConnector.bsc.ts new file mode 100644 index 00000000..c77d53b7 --- /dev/null +++ b/packages/connectors/test/socket/SocketConnector.bsc.ts @@ -0,0 +1,33 @@ +import { deploy, fp, tokens } from '@mimic-fi/v3-helpers' + +import { itBehavesLikeSocketConnector } from './SocketConnector.behavior' + +/* eslint-disable no-secrets/no-secrets */ + +const SOCKET_GATEWAY = '0x3a23F943181408EAC424116Af7b7790c94Cb97a5' + +const WHALE = '0x8894e0a0c962cb723c1976a4421c95949be2d4e3' + +describe('SocketConnector', () => { + const fromChainId = 56 + const fromToken = tokens.bsc.USDC + const fromAmount = fp(50000) + + before('create socket connector', async function () { + this.connector = await deploy('SocketConnector', [SOCKET_GATEWAY]) + }) + + context('to mainnet', () => { + const toChainId = 1 + const toToken = tokens.mainnet.USDC + + itBehavesLikeSocketConnector(fromChainId, fromToken, fromAmount, toChainId, toToken, WHALE) + }) + + context('to optimism', () => { + const toChainId = 10 + const toToken = tokens.optimism.USDC + + itBehavesLikeSocketConnector(fromChainId, fromToken, fromAmount, toChainId, toToken, WHALE) + }) +})