Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Connectors: Implement socket bridge connector #161

Merged
merged 1 commit into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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;
}
54 changes: 54 additions & 0 deletions packages/connectors/contracts/socket/SocketConnector.sol
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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);
}
}
2 changes: 1 addition & 1 deletion packages/connectors/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
73 changes: 73 additions & 0 deletions packages/connectors/src/socket.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
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<QuoteResponse> {
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<TransactionDataResponse> {
return axios.post(
`${SOCKET_URL}/build-tx`,
{ route },
{
headers: {
'API-KEY': SOCKET_API_KEY,
Accept: 'application/json',
},
}
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 1 addition & 6 deletions packages/connectors/test/connext/ConnextConnector.bsc.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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)
})
})
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"fromChainId": 56,
"fromToken": "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
"fromAmount": "50000000000000000000000",
"toChainId": 1,
"toToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"slippage": 0.02,
"data": "0x0000000d52106ce9345fa8d86eeffdd25c389441ff96e05cf90592de8ac76a51cc950d9822d68b83fe1ad97b32cd580d0000000000000a968163f0a57b4000000000000100b8d397000068dc"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"fromChainId": 56,
"fromToken": "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
"fromAmount": "50000000000000000000000",
"toChainId": 10,
"toToken": "0x7F5c764cBc14f9669B88837ca1490cCa17c31607",
"slippage": 0.02,
"data": "0x0000000d52106ce9345fa8d86eeffdd25c389441ff96e05cf90592de8ac76a51cc950d9822d68b83fe1ad97b32cd580d0000000000000a968163f0a57b4000000000000a00b8e5120000620c"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"fromChainId": 56,
"fromToken": "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
"fromAmount": "50000000000000000000000",
"toChainId": 1,
"toToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"slippage": 0.02,
"data": "0x0000000d52106ce94a1bf945c2995b53a00df694fab85f453e5e1f5e8ac76a51cc950d9822d68b83fe1ad97b32cd580d0000000000000a968163f0a57b4000000000000100d2437d000068dd"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"fromChainId": 56,
"fromToken": "0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d",
"fromAmount": "50000000000000000000000",
"toChainId": 10,
"toToken": "0x7F5c764cBc14f9669B88837ca1490cCa17c31607",
"slippage": 0.02,
"data": "0x0000000d52106ce94a1bf945c2995b53a00df694fab85f453e5e1f5e8ac76a51cc950d9822d68b83fe1ad97b32cd580d0000000000000a968163f0a57b4000000000000a00d256670000620c"
}
84 changes: 84 additions & 0 deletions packages/connectors/test/helpers/socket/index.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
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<Fixture | undefined> {
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<void> {
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))
}
Loading
Loading