Skip to content

Commit

Permalink
Connectors: Implement Bebop swap connector (#150)
Browse files Browse the repository at this point in the history
  • Loading branch information
lgalende committed May 22, 2024
1 parent fde8ba7 commit 018af86
Show file tree
Hide file tree
Showing 18 changed files with 415 additions and 13 deletions.
4 changes: 3 additions & 1 deletion .github/scripts/setup-hardhat-config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ AVALANCHE_URL="$6"
BSC_URL="$7"
FANTOM_URL="$8"
ZKEVM_URL="$9"
BASE_URL="${10}"

set -o errexit

Expand All @@ -24,7 +25,8 @@ echo "
\"avalanche\": { \"url\": \"${AVALANCHE_URL}\" },
\"bsc\": { \"url\": \"${BSC_URL}\" },
\"fantom\": { \"url\": \"${FANTOM_URL}\" },
\"zkevm\": { \"url\": \"${ZKEVM_URL}\" }
\"zkevm\": { \"url\": \"${ZKEVM_URL}\" },
\"base\": { \"url\": \"${BASE_URL}\" }
}
}
" > $HOME/.hardhat/networks.mimic.json
6 changes: 4 additions & 2 deletions .github/workflows/ci-connectors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
- name: Set up environment
uses: ./.github/actions/setup
- name: Set up hardhat config
run: .github/scripts/setup-hardhat-config.sh ${{secrets.MAINNET_RPC}} ${{secrets.POLYGON_RPC}} ${{secrets.OPTIMISM_RPC}} ${{secrets.ARBITRUM_RPC}} ${{secrets.GNOSIS_RPC}} ${{secrets.AVALANCHE_RPC}} ${{secrets.BSC_RPC}} ${{secrets.FANTOM_RPC}} ${{secrets.ZKEVM_RPC}}
run: .github/scripts/setup-hardhat-config.sh ${{secrets.MAINNET_RPC}} ${{secrets.POLYGON_RPC}} ${{secrets.OPTIMISM_RPC}} ${{secrets.ARBITRUM_RPC}} ${{secrets.GNOSIS_RPC}} ${{secrets.AVALANCHE_RPC}} ${{secrets.BSC_RPC}} ${{secrets.FANTOM_RPC}} ${{secrets.ZKEVM_RPC}} ${{secrets.BASE_RPC}}
- name: Build
run: yarn build
- name: Test mainnet
Expand All @@ -64,4 +64,6 @@ jobs:
- name: Test fantom
run: yarn workspace @mimic-fi/v3-connectors test:fantom
- name: Test zkevm
run: yarn workspace @mimic-fi/v3-connectors test:zkevm
run: yarn workspace @mimic-fi/v3-connectors test:zkevm
- name: Test base
run: yarn workspace @mimic-fi/v3-connectors test:base
68 changes: 68 additions & 0 deletions packages/connectors/contracts/bebop/BebopConnector.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// 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/token/ERC20/IERC20.sol';
import '@openzeppelin/contracts/utils/Address.sol';

import '@mimic-fi/v3-helpers/contracts/utils/ERC20Helpers.sol';

import '../interfaces/bebop/IBebopConnector.sol';

/**
* @title BebopConnector
* @dev Interfaces with Bebop to swap tokens
*/
contract BebopConnector is IBebopConnector {
// Reference to Bebop Settlement contract
address public immutable override bebopSettlement;

/**
* @dev Creates a new BebopConnector contract
* @param _bebopSettlement Address of Bebop Settlement contract
*/
constructor(address _bebopSettlement) {
bebopSettlement = _bebopSettlement;
}

/**
* @dev Executes a token swap using Bebop
* @param tokenIn Token being sent
* @param tokenOut Token being received
* @param amountIn Amount of tokenIn being swapped
* @param minAmountOut Minimum amount of tokenOut willing to receive
* @param data Calldata to be sent to the Bebop Settlement contract
*/
function execute(address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut, bytes memory data)
external
returns (uint256 amountOut)
{
if (tokenIn == tokenOut) revert BebopSwapSameToken(tokenIn);

uint256 preBalanceIn = IERC20(tokenIn).balanceOf(address(this));
uint256 preBalanceOut = IERC20(tokenOut).balanceOf(address(this));

ERC20Helpers.approve(tokenIn, bebopSettlement, amountIn);
Address.functionCall(bebopSettlement, data, 'BEBOP_SWAP_FAILED');

uint256 postBalanceIn = IERC20(tokenIn).balanceOf(address(this));
bool isPostBalanceInUnexpected = postBalanceIn < preBalanceIn - amountIn;
if (isPostBalanceInUnexpected) revert BebopBadPostTokenInBalance(postBalanceIn, preBalanceIn, amountIn);

uint256 postBalanceOut = IERC20(tokenOut).balanceOf(address(this));
amountOut = postBalanceOut - preBalanceOut;
if (amountOut < minAmountOut) revert BebopBadAmountOut(amountOut, minAmountOut);
}
}
52 changes: 52 additions & 0 deletions packages/connectors/contracts/interfaces/bebop/IBebopConnector.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// 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 Bebop connector interface
*/
interface IBebopConnector {
/**
* @dev The token in is the same as the token out
*/
error BebopSwapSameToken(address token);

/**
* @dev The amount out is lower than the minimum amount out
*/
error BebopBadAmountOut(uint256 amountOut, uint256 minAmountOut);

/**
* @dev The post token in balance is lower than the previous token in balance minus the amount in
*/
error BebopBadPostTokenInBalance(uint256 postBalanceIn, uint256 preBalanceIn, uint256 amountIn);

/**
* @dev Tells the reference to Bebop Settlement contract
*/
function bebopSettlement() external view returns (address);

/**
* @dev Executes a token swap using Bebop
* @param tokenIn Token to be sent
* @param tokenOut Token to be received
* @param amountIn Amount of token in to be swapped
* @param minAmountOut Minimum amount of token out willing to receive
* @param data Calldata to be sent to the Bebop Settlement contract
*/
function execute(address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut, bytes memory data)
external
returns (uint256 amountOut);
}
3 changes: 2 additions & 1 deletion packages/connectors/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@
"test:mainnet": "yarn test --fork mainnet --block-number 17525323 --chain-id 1",
"test:polygon": "yarn test --fork polygon --block-number 44153231 --chain-id 137",
"test:optimism": "yarn test --fork optimism --block-number 105914596 --chain-id 10",
"test:arbitrum": "yarn test --fork arbitrum --block-number 105116582 --chain-id 42161",
"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 27925272 --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 14589312 --chain-id 8453",
"prepare": "yarn build"
},
"dependencies": {
Expand Down
53 changes: 53 additions & 0 deletions packages/connectors/src/bebop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import axios, { AxiosError } from 'axios'
import { BigNumber, Contract } from 'ethers'

const BEBOP_URL = 'https://api.bebop.xyz/pmm'

const CHAIN_NAMES = {
42161: 'arbitrum',
8453: 'base',
}

export type SwapResponse = { data: { tx: { data: string } } }

export async function getBebopSwapData(
chainId: number,
sender: Contract,
tokenIn: Contract,
tokenOut: Contract,
amountIn: BigNumber
): Promise<string> {
try {
const response = await getSwap(chainId, sender, tokenIn, tokenOut, amountIn)
return response.data.tx.data
} catch (error) {
if (error instanceof AxiosError) throw Error(error.toString() + ' - ' + error.response?.data?.description)
else throw error
}
}

async function getSwap(
chainId: number,
sender: Contract,
tokenIn: Contract,
tokenOut: Contract,
amountIn: BigNumber
): Promise<SwapResponse> {
const chainName = CHAIN_NAMES[chainId]
if (!chainName) throw Error('Unsupported chain id')

return axios.get(`${BEBOP_URL}/${chainName}/v3/quote`, {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
params: {
taker_address: sender.address,
sell_tokens: tokenIn.address,
buy_tokens: tokenOut.address,
sell_amounts: amountIn.toString(),
gasless: false,
skip_validation: true,
},
})
}
2 changes: 1 addition & 1 deletion packages/connectors/src/hop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const HOP_URL = 'https://api.hop.exchange/v1'
export type QuoteResponse = { data: { bonderFee: string; error: string } }

const CHAINS: { [key: number]: string } = {
1: 'mainnet',
1: 'ethereum',
137: 'polygon',
100: 'gnosis',
10: 'optimism',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export function itBehavesLikeBalancerV2SwapConnector(
const hopTokens = []

context('USDC-WETH', () => {
const amountIn = toUSDC(10e3)
const amountIn = toUSDC(100)

it('swaps correctly', async function () {
const previousBalance = await weth.balanceOf(this.connector.address)
Expand Down Expand Up @@ -94,7 +94,7 @@ export function itBehavesLikeBalancerV2SwapConnector(
})

context('WBTC-USDC', () => {
const amountIn = toWBTC(1)
const amountIn = toWBTC(0.1)
const hopPoolIds = [WETH_USDC_POOL_ID]

it('swaps correctly', async function () {
Expand Down
25 changes: 25 additions & 0 deletions packages/connectors/test/bebop/BebopConnector.arbitrum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { deploy } from '@mimic-fi/v3-helpers'

import { itBehavesLikeBebopConnector } from './BebopConnector.behavior'

/* eslint-disable no-secrets/no-secrets */

const CHAIN = 42161

const USDC = '0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8'
const WETH = '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1'
const WHALE = '0xEeBe760354F5dcBa195EDe0a3B93901441D0968F'

const BEBOP_SETTLEMENT = '0xbbbbbBB520d69a9775E85b458C58c648259FAD5F'

const CHAINLINK_ETH_USD = '0x639fe6ab55c921f74e7fac1ee960c0b6293ba612'

describe('BebopConnector', () => {
const SLIPPAGE = 0.015

before('create bebop connector', async function () {
this.connector = await deploy('BebopConnector', [BEBOP_SETTLEMENT])
})

itBehavesLikeBebopConnector(CHAIN, USDC, WETH, WHALE, SLIPPAGE, CHAINLINK_ETH_USD)
})
25 changes: 25 additions & 0 deletions packages/connectors/test/bebop/BebopConnector.base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { deploy } from '@mimic-fi/v3-helpers'

import { itBehavesLikeBebopConnector } from './BebopConnector.behavior'

/* eslint-disable no-secrets/no-secrets */

const CHAIN = 8453

const USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'
const WETH = '0x4200000000000000000000000000000000000006'
const WHALE = '0xec8d8D4b215727f3476FF0ab41c406FA99b4272C'

const BEBOP_SETTLEMENT = '0xbbbbbBB520d69a9775E85b458C58c648259FAD5F'

const CHAINLINK_ETH_USD = '0x71041dddad3595F9CEd3DcCFBe3D1F4b0a16Bb70'

describe('BebopConnector', () => {
const SLIPPAGE = 0.015

before('create bebop connector', async function () {
this.connector = await deploy('BebopConnector', [BEBOP_SETTLEMENT])
})

itBehavesLikeBebopConnector(CHAIN, USDC, WETH, WHALE, SLIPPAGE, CHAINLINK_ETH_USD)
})
76 changes: 76 additions & 0 deletions packages/connectors/test/bebop/BebopConnector.behavior.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { deployProxy, fp, impersonate, instanceAt, pct, toUSDC, ZERO_ADDRESS } from '@mimic-fi/v3-helpers'
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'
import { expect } from 'chai'
import { BigNumber, Contract } from 'ethers'

import { loadOrGetBebopSwapData } from '../helpers/bebop'

export function itBehavesLikeBebopConnector(
CHAIN: number,
USDC: string,
WETH: string,
WHALE: string,
SLIPPAGE: number,
CHAINLINK_ETH_USD: string
): void {
let weth: Contract, usdc: Contract, whale: SignerWithAddress, priceOracle: Contract

before('load tokens and accounts', async function () {
weth = await instanceAt('IERC20Metadata', WETH)
usdc = await instanceAt('IERC20Metadata', USDC)
whale = await impersonate(WHALE, fp(100))
})

before('create price oracle', async function () {
priceOracle = await deployProxy(
'@mimic-fi/v3-price-oracle/artifacts/contracts/PriceOracle.sol/PriceOracle',
[],
[ZERO_ADDRESS, ZERO_ADDRESS, USDC, [{ base: WETH, quote: USDC, feed: CHAINLINK_ETH_USD }]]
)
})

const getExpectedMinAmountOut = async (
tokenIn: string,
tokenOut: string,
amountIn: BigNumber,
slippage: number
): Promise<BigNumber> => {
const price = await priceOracle['getPrice(address,address)'](tokenIn, tokenOut)
const expectedAmountOut = price.mul(amountIn).div(fp(1))
return expectedAmountOut.sub(pct(expectedAmountOut, slippage))
}

context('USDC-WETH', () => {
const amountIn = toUSDC(10e3)

it('swaps correctly USDC-WETH', async function () {
const previousBalance = await weth.balanceOf(this.connector.address)
await usdc.connect(whale).transfer(this.connector.address, amountIn)

const minAmountOut = 0
const data = await loadOrGetBebopSwapData(CHAIN, this.connector, usdc, weth, amountIn)
await this.connector.connect(whale).execute(USDC, WETH, amountIn, minAmountOut, data)

const currentBalance = await weth.balanceOf(this.connector.address)
const expectedMinAmountOut = await getExpectedMinAmountOut(USDC, WETH, amountIn, SLIPPAGE)
expect(currentBalance.sub(previousBalance)).to.be.at.least(expectedMinAmountOut)
})
})

context('WETH-USDC', () => {
const amountIn = fp(1)

it('swaps correctly WETH-USDC', async function () {
const previousBalance = await usdc.balanceOf(this.connector.address)
await weth.connect(whale).transfer(this.connector.address, amountIn)

const minAmountOut = 0
const data = await loadOrGetBebopSwapData(CHAIN, this.connector, weth, usdc, amountIn)
await this.connector.connect(whale).execute(WETH, USDC, amountIn, minAmountOut, data)

const currentBalance = await usdc.balanceOf(this.connector.address)
const expectedMinAmountOut = await getExpectedMinAmountOut(WETH, USDC, amountIn, SLIPPAGE)
expect(currentBalance.sub(previousBalance)).to.be.at.least(expectedMinAmountOut)
})
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"tokenIn": "0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8",
"tokenOut": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
"amountIn": "10000000000",
"data": "0x4dcebcba000000000000000000000000000000000000000000000000000000006647a4370000000000000000000000004f0a4ce902616f0e16a7c31075aa5601779ad40500000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f0000000000000000000000000000000000000000000000000000031ef93f4f3b000000000000000000000000ff970a61a04b1ca14834a43f5de4533ebddb5cc800000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab100000000000000000000000000000000000000000000000000000002540be4000000000000000000000000000000000000000000000000002d0f5f4319e062100000000000000000000000004f0a4ce902616f0e16a7c31075aa5601779ad40500000000000000000000000000000000000000000000000000000000000000007409e45e4bca4e8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000041791c1ef4b257ba835057f84321c9e9804a94a1352b8a3961ed1d656fdab3d77f0f8ba4428da58c1c91fcd28472f6e8fce0ba10f9068f1d727e62ce1dbb405cc11b00000000000000000000000000000000000000000000000000000000000000"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"tokenIn": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1",
"tokenOut": "0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8",
"amountIn": "1000000000000000000",
"data": "0x4dcebcba000000000000000000000000000000000000000000000000000000006647a4390000000000000000000000004f0a4ce902616f0e16a7c31075aa5601779ad40500000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f0000000000000000000000000000000000000000000000000000031ef93f4f3c00000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab1000000000000000000000000ff970a61a04b1ca14834a43f5de4533ebddb5cc80000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000b7761b950000000000000000000000004f0a4ce902616f0e16a7c31075aa5601779ad40500000000000000000000000000000000000000000000000000000000000000004087748dff5aa5e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000418ad1282832998747d3a68672463c5855f61e09a2485d7505c42d2fddaa998a39077cdf67c41cc05c97b49602b9c2412f0cfd8d94c3ff2869b9d146ea9843d9691b00000000000000000000000000000000000000000000000000000000000000"
}
Loading

0 comments on commit 018af86

Please sign in to comment.