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 Bebop swap connector #150

Merged
merged 3 commits into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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"
lgalende marked this conversation as resolved.
Show resolved Hide resolved

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
52 changes: 52 additions & 0 deletions packages/connectors/src/bebop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import axios, { AxiosError } from 'axios'
import { BigNumber, Contract } from 'ethers'

const BEBOP_URL = 'https://api.bebop.xyz/pmm'
const CHAIN_NAMES = {
lgalende marked this conversation as resolved.
Show resolved Hide resolved
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,
},
})
}
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"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"tokenIn": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"tokenOut": "0x4200000000000000000000000000000000000006",
"amountIn": "10000000000",
"data": "0x4dcebcba000000000000000000000000000000000000000000000000000000006647a7a600000000000000000000000034b40ba116d5dec75548a9e9a8f15411461e8c7000000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f0000000000000000000000000000000000000000000000000000031ef93f5bf7000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913000000000000000000000000420000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000002540be4000000000000000000000000000000000000000000000000002ce1a31db374011a00000000000000000000000034b40ba116d5dec75548a9e9a8f15411461e8c70000000000000000000000000000000000000000000000000000000000000000085543df12d753b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000410503ae0427d1300d5d9092c65def74ef08b144804dd062976cde63a2813ece05729017fa0ccc9ac2b9587a5a347d189aac529cf55b65185a9585e2f4f50b640d1c00000000000000000000000000000000000000000000000000000000000000"
}
Loading
Loading