From 2700c73191d7d6a99d98cc7d7f79466a588d3001 Mon Sep 17 00:00:00 2001 From: lgalende Date: Fri, 17 May 2024 15:53:15 -0300 Subject: [PATCH 1/2] connectors: implement bebop swap connector --- .github/scripts/setup-hardhat-config.sh | 4 +- .github/workflows/ci-connectors.yml | 6 +- .../contracts/bebop/BebopConnector.sol | 68 +++++++++++++++++ .../interfaces/bebop/IBebopConnector.sol | 52 +++++++++++++ packages/connectors/package.json | 3 +- packages/connectors/src/bebop.ts | 52 +++++++++++++ .../BalancerV2SwapConnector.behavior.ts | 4 +- .../test/bebop/BebopConnector.arbitrum.ts | 25 ++++++ .../test/bebop/BebopConnector.base.ts | 25 ++++++ .../test/bebop/BebopConnector.behavior.ts | 76 +++++++++++++++++++ .../fixtures/42161/212259071/USDC-WETH.json | 6 ++ .../fixtures/42161/212259071/WETH-USDC.json | 6 ++ .../fixtures/8453/14589312/USDC-WETH.json | 6 ++ .../fixtures/8453/14589312/WETH-USDC.json | 6 ++ .../connectors/test/helpers/bebop/index.ts | 74 ++++++++++++++++++ .../test/hop/HopSwapConnector.arbitrum.ts | 2 +- 16 files changed, 408 insertions(+), 7 deletions(-) create mode 100644 packages/connectors/contracts/bebop/BebopConnector.sol create mode 100644 packages/connectors/contracts/interfaces/bebop/IBebopConnector.sol create mode 100644 packages/connectors/src/bebop.ts create mode 100644 packages/connectors/test/bebop/BebopConnector.arbitrum.ts create mode 100644 packages/connectors/test/bebop/BebopConnector.base.ts create mode 100644 packages/connectors/test/bebop/BebopConnector.behavior.ts create mode 100644 packages/connectors/test/helpers/bebop/fixtures/42161/212259071/USDC-WETH.json create mode 100644 packages/connectors/test/helpers/bebop/fixtures/42161/212259071/WETH-USDC.json create mode 100644 packages/connectors/test/helpers/bebop/fixtures/8453/14589312/USDC-WETH.json create mode 100644 packages/connectors/test/helpers/bebop/fixtures/8453/14589312/WETH-USDC.json create mode 100644 packages/connectors/test/helpers/bebop/index.ts diff --git a/.github/scripts/setup-hardhat-config.sh b/.github/scripts/setup-hardhat-config.sh index 393f6d3a..6c1d649e 100755 --- a/.github/scripts/setup-hardhat-config.sh +++ b/.github/scripts/setup-hardhat-config.sh @@ -8,6 +8,7 @@ AVALANCHE_URL="$6" BSC_URL="$7" FANTOM_URL="$8" ZKEVM_URL="$9" +BASE_URL="$10" set -o errexit @@ -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 diff --git a/.github/workflows/ci-connectors.yml b/.github/workflows/ci-connectors.yml index b49593d7..32735cbd 100644 --- a/.github/workflows/ci-connectors.yml +++ b/.github/workflows/ci-connectors.yml @@ -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 @@ -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 \ No newline at end of file + run: yarn workspace @mimic-fi/v3-connectors test:zkevm + - name: Test base + run: yarn workspace @mimic-fi/v3-connectors test:base \ No newline at end of file diff --git a/packages/connectors/contracts/bebop/BebopConnector.sol b/packages/connectors/contracts/bebop/BebopConnector.sol new file mode 100644 index 00000000..310f74ae --- /dev/null +++ b/packages/connectors/contracts/bebop/BebopConnector.sol @@ -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 . + +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); + } +} diff --git a/packages/connectors/contracts/interfaces/bebop/IBebopConnector.sol b/packages/connectors/contracts/interfaces/bebop/IBebopConnector.sol new file mode 100644 index 00000000..fca643f7 --- /dev/null +++ b/packages/connectors/contracts/interfaces/bebop/IBebopConnector.sol @@ -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 . + +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); +} diff --git a/packages/connectors/package.json b/packages/connectors/package.json index fed6f5b5..cd38cbd8 100644 --- a/packages/connectors/package.json +++ b/packages/connectors/package.json @@ -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": { diff --git a/packages/connectors/src/bebop.ts b/packages/connectors/src/bebop.ts new file mode 100644 index 00000000..c15e746b --- /dev/null +++ b/packages/connectors/src/bebop.ts @@ -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 = { + 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 { + 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 { + 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, + }, + }) +} diff --git a/packages/connectors/test/balancer/BalancerV2SwapConnector.behavior.ts b/packages/connectors/test/balancer/BalancerV2SwapConnector.behavior.ts index 90d76a31..fea3d614 100644 --- a/packages/connectors/test/balancer/BalancerV2SwapConnector.behavior.ts +++ b/packages/connectors/test/balancer/BalancerV2SwapConnector.behavior.ts @@ -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) @@ -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 () { diff --git a/packages/connectors/test/bebop/BebopConnector.arbitrum.ts b/packages/connectors/test/bebop/BebopConnector.arbitrum.ts new file mode 100644 index 00000000..6d23ff33 --- /dev/null +++ b/packages/connectors/test/bebop/BebopConnector.arbitrum.ts @@ -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) +}) diff --git a/packages/connectors/test/bebop/BebopConnector.base.ts b/packages/connectors/test/bebop/BebopConnector.base.ts new file mode 100644 index 00000000..31be5a62 --- /dev/null +++ b/packages/connectors/test/bebop/BebopConnector.base.ts @@ -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) +}) diff --git a/packages/connectors/test/bebop/BebopConnector.behavior.ts b/packages/connectors/test/bebop/BebopConnector.behavior.ts new file mode 100644 index 00000000..c21d6fcd --- /dev/null +++ b/packages/connectors/test/bebop/BebopConnector.behavior.ts @@ -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 => { + 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) + }) + }) +} diff --git a/packages/connectors/test/helpers/bebop/fixtures/42161/212259071/USDC-WETH.json b/packages/connectors/test/helpers/bebop/fixtures/42161/212259071/USDC-WETH.json new file mode 100644 index 00000000..9affdf68 --- /dev/null +++ b/packages/connectors/test/helpers/bebop/fixtures/42161/212259071/USDC-WETH.json @@ -0,0 +1,6 @@ +{ + "tokenIn": "0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8", + "tokenOut": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", + "amountIn": "10000000000", + "data": "0x4dcebcba000000000000000000000000000000000000000000000000000000006647a4370000000000000000000000004f0a4ce902616f0e16a7c31075aa5601779ad40500000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f0000000000000000000000000000000000000000000000000000031ef93f4f3b000000000000000000000000ff970a61a04b1ca14834a43f5de4533ebddb5cc800000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab100000000000000000000000000000000000000000000000000000002540be4000000000000000000000000000000000000000000000000002d0f5f4319e062100000000000000000000000004f0a4ce902616f0e16a7c31075aa5601779ad40500000000000000000000000000000000000000000000000000000000000000007409e45e4bca4e8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000041791c1ef4b257ba835057f84321c9e9804a94a1352b8a3961ed1d656fdab3d77f0f8ba4428da58c1c91fcd28472f6e8fce0ba10f9068f1d727e62ce1dbb405cc11b00000000000000000000000000000000000000000000000000000000000000" +} \ No newline at end of file diff --git a/packages/connectors/test/helpers/bebop/fixtures/42161/212259071/WETH-USDC.json b/packages/connectors/test/helpers/bebop/fixtures/42161/212259071/WETH-USDC.json new file mode 100644 index 00000000..84ff1896 --- /dev/null +++ b/packages/connectors/test/helpers/bebop/fixtures/42161/212259071/WETH-USDC.json @@ -0,0 +1,6 @@ +{ + "tokenIn": "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1", + "tokenOut": "0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8", + "amountIn": "1000000000000000000", + "data": "0x4dcebcba000000000000000000000000000000000000000000000000000000006647a4390000000000000000000000004f0a4ce902616f0e16a7c31075aa5601779ad40500000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f0000000000000000000000000000000000000000000000000000031ef93f4f3c00000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab1000000000000000000000000ff970a61a04b1ca14834a43f5de4533ebddb5cc80000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000b7761b950000000000000000000000004f0a4ce902616f0e16a7c31075aa5601779ad40500000000000000000000000000000000000000000000000000000000000000004087748dff5aa5e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000418ad1282832998747d3a68672463c5855f61e09a2485d7505c42d2fddaa998a39077cdf67c41cc05c97b49602b9c2412f0cfd8d94c3ff2869b9d146ea9843d9691b00000000000000000000000000000000000000000000000000000000000000" +} \ No newline at end of file diff --git a/packages/connectors/test/helpers/bebop/fixtures/8453/14589312/USDC-WETH.json b/packages/connectors/test/helpers/bebop/fixtures/8453/14589312/USDC-WETH.json new file mode 100644 index 00000000..df5ae968 --- /dev/null +++ b/packages/connectors/test/helpers/bebop/fixtures/8453/14589312/USDC-WETH.json @@ -0,0 +1,6 @@ +{ + "tokenIn": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "tokenOut": "0x4200000000000000000000000000000000000006", + "amountIn": "10000000000", + "data": "0x4dcebcba000000000000000000000000000000000000000000000000000000006647a7a600000000000000000000000034b40ba116d5dec75548a9e9a8f15411461e8c7000000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f0000000000000000000000000000000000000000000000000000031ef93f5bf7000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda02913000000000000000000000000420000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000002540be4000000000000000000000000000000000000000000000000002ce1a31db374011a00000000000000000000000034b40ba116d5dec75548a9e9a8f15411461e8c70000000000000000000000000000000000000000000000000000000000000000085543df12d753b0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000410503ae0427d1300d5d9092c65def74ef08b144804dd062976cde63a2813ece05729017fa0ccc9ac2b9587a5a347d189aac529cf55b65185a9585e2f4f50b640d1c00000000000000000000000000000000000000000000000000000000000000" +} \ No newline at end of file diff --git a/packages/connectors/test/helpers/bebop/fixtures/8453/14589312/WETH-USDC.json b/packages/connectors/test/helpers/bebop/fixtures/8453/14589312/WETH-USDC.json new file mode 100644 index 00000000..f458a55f --- /dev/null +++ b/packages/connectors/test/helpers/bebop/fixtures/8453/14589312/WETH-USDC.json @@ -0,0 +1,6 @@ +{ + "tokenIn": "0x4200000000000000000000000000000000000006", + "tokenOut": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "amountIn": "1000000000000000000", + "data": "0x4dcebcba000000000000000000000000000000000000000000000000000000006647a7a700000000000000000000000034b40ba116d5dec75548a9e9a8f15411461e8c7000000000000000000000000051c72848c68a965f66fa7a88855f9f7784502a7f0000000000000000000000000000000000000000000000000000031ef93f5bf90000000000000000000000004200000000000000000000000000000000000006000000000000000000000000833589fcd6edb6e08f4c7c32d4f71b54bda029130000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000b7b369c400000000000000000000000034b40ba116d5dec75548a9e9a8f15411461e8c70000000000000000000000000000000000000000000000000000000000000000006a0f9771de8210c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000041252d2f60f204dfa8c3d4ed6950905708d4c6f98d514adaa64fb8fc45f009e0c473ea6273fc4ab2f42c3a1bdb3236978ddca8d36ec1494878f9067f2f995493291b00000000000000000000000000000000000000000000000000000000000000" +} \ No newline at end of file diff --git a/packages/connectors/test/helpers/bebop/index.ts b/packages/connectors/test/helpers/bebop/index.ts new file mode 100644 index 00000000..e92321c5 --- /dev/null +++ b/packages/connectors/test/helpers/bebop/index.ts @@ -0,0 +1,74 @@ +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 { getBebopSwapData } from '../../../src/bebop' + +type Fixture = { + tokenIn: string + tokenOut: string + amountIn: string + slippage: number + data: string +} + +export async function loadOrGetBebopSwapData( + chainId: number, + sender: Contract, + tokenIn: Contract, + tokenOut: Contract, + amountIn: BigNumber +): Promise { + const config = hre.network.config as HardhatNetworkConfig + const blockNumber = config?.forking?.blockNumber?.toString() || (await currentBlockNumber()).toString() + + const fixture = await readFixture(chainId, tokenIn, tokenOut, blockNumber) + if (fixture) return fixture.data + + const data = await getBebopSwapData(chainId, sender, tokenIn, tokenOut, amountIn) + await saveFixture(chainId, tokenIn, tokenOut, amountIn, data, blockNumber) + return data +} + +async function readFixture( + chainId: number, + tokenIn: Contract, + tokenOut: Contract, + blockNumber: string +): Promise { + const swapPath = `${await tokenIn.symbol()}-${await tokenOut.symbol()}.json` + const fixturePath = path.join(__dirname, 'fixtures', chainId.toString(), blockNumber, swapPath) + if (!fs.existsSync(fixturePath)) return undefined + return JSON.parse(fs.readFileSync(fixturePath).toString()) +} + +async function saveFixture( + chainId: number, + tokenIn: Contract, + tokenOut: Contract, + amountIn: BigNumber, + data: string, + blockNumber: string +): Promise { + const output = { + tokenIn: tokenIn.address, + tokenOut: tokenOut.address, + amountIn: amountIn.toString(), + data, + } + + const fixturesPath = path.join(__dirname, 'fixtures') + if (!fs.existsSync(fixturesPath)) fs.mkdirSync(fixturesPath) + + const networkPath = path.join(fixturesPath, chainId.toString()) + if (!fs.existsSync(networkPath)) fs.mkdirSync(networkPath) + + const blockNumberPath = path.join(networkPath, blockNumber) + if (!fs.existsSync(blockNumberPath)) fs.mkdirSync(blockNumberPath) + + const swapPath = path.join(blockNumberPath, `${await tokenIn.symbol()}-${await tokenOut.symbol()}.json`) + fs.writeFileSync(swapPath, JSON.stringify(output, null, 2)) +} diff --git a/packages/connectors/test/hop/HopSwapConnector.arbitrum.ts b/packages/connectors/test/hop/HopSwapConnector.arbitrum.ts index f4545ab2..6d61f473 100644 --- a/packages/connectors/test/hop/HopSwapConnector.arbitrum.ts +++ b/packages/connectors/test/hop/HopSwapConnector.arbitrum.ts @@ -12,7 +12,7 @@ const WHALE = '0x5bdf85216ec1e38d6458c870992a69e38e03f7ef' const HOP_USDC_DEX = '0x10541b07d8ad2647dc6cd67abd4c03575dade261' describe('HopSwapConnector', () => { - const SLIPPAGE = 0.01 + const SLIPPAGE = 0.015 before('create hop swap connector', async function () { this.connector = await deploy('HopSwapConnector') From d31328e74a4452fce1580b7b3bae78086e7f1b66 Mon Sep 17 00:00:00 2001 From: lgalende Date: Sat, 18 May 2024 13:53:46 -0300 Subject: [PATCH 2/2] tasks: implement bebop swap task --- .../interfaces/swap/IBebopSwapper.sol | 27 ++ .../tasks/contracts/swap/BebopSwapper.sol | 113 +++++ .../test/swap/BebopConnectorMock.sol | 27 ++ packages/tasks/test/swap/BebopSwapper.test.ts | 439 ++++++++++++++++++ 4 files changed, 606 insertions(+) create mode 100644 packages/tasks/contracts/interfaces/swap/IBebopSwapper.sol create mode 100644 packages/tasks/contracts/swap/BebopSwapper.sol create mode 100644 packages/tasks/contracts/test/swap/BebopConnectorMock.sol create mode 100644 packages/tasks/test/swap/BebopSwapper.test.ts diff --git a/packages/tasks/contracts/interfaces/swap/IBebopSwapper.sol b/packages/tasks/contracts/interfaces/swap/IBebopSwapper.sol new file mode 100644 index 00000000..3aa34e17 --- /dev/null +++ b/packages/tasks/contracts/interfaces/swap/IBebopSwapper.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 './IBaseSwapTask.sol'; + +/** + * @dev Bebop swapper interface + */ +interface IBebopSwapper is IBaseSwapTask { + /** + * @dev Execution function + */ + function call(address tokenIn, uint256 amountIn, uint256 slippage, bytes memory data) external; +} diff --git a/packages/tasks/contracts/swap/BebopSwapper.sol b/packages/tasks/contracts/swap/BebopSwapper.sol new file mode 100644 index 00000000..0e5f8ae9 --- /dev/null +++ b/packages/tasks/contracts/swap/BebopSwapper.sol @@ -0,0 +1,113 @@ +// 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-helpers/contracts/utils/BytesHelpers.sol'; +import '@mimic-fi/v3-connectors/contracts/interfaces/bebop/IBebopConnector.sol'; + +import './BaseSwapTask.sol'; +import '../interfaces/swap/IBebopSwapper.sol'; + +/** + * @title Bebop swapper + * @dev Task that extends the base swap task to use Bebop + */ +contract BebopSwapper is IBebopSwapper, BaseSwapTask { + using FixedPoint for uint256; + using BytesHelpers for bytes; + + // Execution type for relayers + bytes32 public constant override EXECUTION_TYPE = keccak256('BEBOP_SWAPPER'); + + /** + * @dev Bebop swap config. Only used in the initializer. + */ + struct BebopSwapConfig { + BaseSwapConfig baseSwapConfig; + } + + /** + * @dev Initializes the Bebop swapper + * @param config Bebop swap config + */ + function initialize(BebopSwapConfig memory config) external virtual initializer { + __BebopSwapper_init(config); + } + + /** + * @dev Initializes the Bebop swapper. It does call upper contracts initializers. + * @param config Bebop swap config + */ + function __BebopSwapper_init(BebopSwapConfig memory config) internal onlyInitializing { + __BaseSwapTask_init(config.baseSwapConfig); + __BebopSwapper_init_unchained(config); + } + + /** + * @dev Initializes the Bebop swapper. It does not call upper contracts initializers. + * @param config Bebop swap config + */ + function __BebopSwapper_init_unchained(BebopSwapConfig memory config) internal onlyInitializing { + // solhint-disable-previous-line no-empty-blocks + } + + /** + * @dev Executes the Bebop swapper + */ + function call(address tokenIn, uint256 amountIn, uint256 slippage, bytes memory data) + external + override + authP(authParams(tokenIn, amountIn, slippage)) + { + if (amountIn == 0) amountIn = getTaskAmount(tokenIn); + _beforeBebopSwapper(tokenIn, amountIn, slippage); + + address tokenOut = getTokenOut(tokenIn); + uint256 price = _getPrice(tokenIn, tokenOut); + uint256 minAmountOut = amountIn.mulUp(price).mulUp(FixedPoint.ONE - slippage); + bytes memory connectorData = abi.encodeWithSelector( + IBebopConnector.execute.selector, + tokenIn, + tokenOut, + amountIn, + minAmountOut, + data + ); + + bytes memory result = ISmartVault(smartVault).execute(connector, connectorData); + _afterBebopSwapper(tokenIn, amountIn, slippage, tokenOut, result.toUint256()); + } + + /** + * @dev Before Bebop swapper hook + */ + function _beforeBebopSwapper(address token, uint256 amount, uint256 slippage) internal virtual { + _beforeBaseSwapTask(token, amount, slippage); + } + + /** + * @dev After Bebop swapper hook + */ + function _afterBebopSwapper( + address tokenIn, + uint256 amountIn, + uint256 slippage, + address tokenOut, + uint256 amountOut + ) internal virtual { + _afterBaseSwapTask(tokenIn, amountIn, slippage, tokenOut, amountOut); + } +} diff --git a/packages/tasks/contracts/test/swap/BebopConnectorMock.sol b/packages/tasks/contracts/test/swap/BebopConnectorMock.sol new file mode 100644 index 00000000..16403b6a --- /dev/null +++ b/packages/tasks/contracts/test/swap/BebopConnectorMock.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; + +contract BebopConnectorMock { + event LogExecute(address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut, bytes data); + + function execute(address tokenIn, address tokenOut, uint256 amountIn, uint256 minAmountOut, bytes memory data) + external + returns (uint256) + { + emit LogExecute(tokenIn, tokenOut, amountIn, minAmountOut, data); + return minAmountOut; + } +} diff --git a/packages/tasks/test/swap/BebopSwapper.test.ts b/packages/tasks/test/swap/BebopSwapper.test.ts new file mode 100644 index 00000000..b498f40d --- /dev/null +++ b/packages/tasks/test/swap/BebopSwapper.test.ts @@ -0,0 +1,439 @@ +import { OP } from '@mimic-fi/v3-authorizer' +import { + assertIndirectEvent, + assertNoEvent, + BigNumberish, + deploy, + deployFeedMock, + deployProxy, + deployTokenMock, + fp, + getSigners, + MAX_UINT256, + ZERO_ADDRESS, +} from '@mimic-fi/v3-helpers' +import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/dist/src/signer-with-address' +import { expect } from 'chai' +import { Contract, ContractTransaction } from 'ethers' +import { defaultAbiCoder } from 'ethers/lib/utils' +import { ethers } from 'hardhat' + +import { buildEmptyTaskConfig, deployEnvironment } from '../../src/setup' +import { itBehavesLikeBaseSwapTask } from './BaseSwapTask.behavior' + +describe('BebopSwapper', () => { + let task: Contract + let smartVault: Contract, authorizer: Contract, priceOracle: Contract, connector: Contract, owner: SignerWithAddress + + before('setup', async () => { + // eslint-disable-next-line prettier/prettier + ([, owner] = await getSigners()) + ;({ authorizer, smartVault, priceOracle } = await deployEnvironment(owner)) + }) + + before('deploy connector', async () => { + connector = await deploy('BebopConnectorMock') + 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( + 'BebopSwapper', + [], + [ + { + baseSwapConfig: { + connector: connector.address, + tokenOut: ZERO_ADDRESS, + maxSlippage: 0, + customTokensOut: [], + customMaxSlippages: [], + taskConfig: buildEmptyTaskConfig(owner, smartVault), + }, + }, + ] + ) + }) + + describe('swapper', () => { + beforeEach('set params', async function () { + this.owner = owner + this.task = task + this.authorizer = authorizer + }) + + itBehavesLikeBaseSwapTask('BEBOP_SWAPPER') + }) + + describe('call', () => { + 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 in is not the zero address', () => { + let tokenIn: Contract + + beforeEach('set token in', async () => { + tokenIn = await deployTokenMock('TKN') + }) + + context('when the amount in is not zero', () => { + const tokenRate = 2 // 1 token in = 2 token out + const thresholdAmount = fp(0.1) // in token out + const thresholdAmountInTokenIn = thresholdAmount.div(tokenRate) // threshold expressed in token in + const amountIn = thresholdAmountInTokenIn + + context('when the token in is allowed', () => { + context('when there is a token out set', () => { + let tokenOut: Contract + let extraCallData = '' + + beforeEach('set default token out', async () => { + tokenOut = await deployTokenMock('TKN') + const setDefaultTokenOutRole = task.interface.getSighash('setDefaultTokenOut') + await authorizer.connect(owner).authorize(owner.address, task.address, setDefaultTokenOutRole, []) + await task.connect(owner).setDefaultTokenOut(tokenOut.address) + }) + + context('when an off-chain oracle is given', () => { + beforeEach('sign off-chain oracle', async () => { + const setSignerRole = priceOracle.interface.getSighash('setSigner') + await authorizer.connect(owner).authorize(owner.address, priceOracle.address, setSignerRole, []) + await priceOracle.connect(owner).setSigner(owner.address, true) + + type PriceData = { base: string; quote: string; rate: BigNumberish; deadline: BigNumberish } + const pricesData: PriceData[] = [ + { + base: tokenIn.address, + quote: tokenOut.address, + rate: fp(tokenRate), + deadline: MAX_UINT256, + }, + { + base: tokenOut.address, + quote: tokenIn.address, + rate: fp(1).mul(fp(1)).div(fp(tokenRate)), + deadline: MAX_UINT256, + }, + ] + + const PricesDataType = 'PriceData(address base, address quote, uint256 rate, uint256 deadline)[]' + const encodedPrices = await defaultAbiCoder.encode([PricesDataType], [pricesData]) + const message = ethers.utils.solidityKeccak256(['bytes'], [encodedPrices]) + const signature = await owner.signMessage(ethers.utils.arrayify(message)) + const data = defaultAbiCoder.encode([PricesDataType, 'bytes'], [pricesData, signature]).slice(2) + const dataLength = defaultAbiCoder.encode(['uint256'], [data.length / 2]).slice(2) + extraCallData = `${data}${dataLength}` + }) + + 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(tokenOut.address, thresholdAmount, 0) + }) + + const executeTask = async (amountIn, slippage, data): Promise => { + const callTx = await task.populateTransaction.call(tokenIn.address, amountIn, slippage, data) + const callData = `${callTx.data}${extraCallData}` + return owner.sendTransaction({ to: task.address, data: callData }) + } + + context('when the smart vault balance passes the threshold', () => { + beforeEach('fund smart vault', async () => { + await tokenIn.mint(smartVault.address, amountIn) + }) + + context('when the slippage is below the limit', () => { + const data = '0xaabb' + const slippage = fp(0.01) + const expectedAmountOut = amountIn.mul(tokenRate) + const minAmountOut = expectedAmountOut.mul(fp(1).sub(slippage)).div(fp(1)) + + beforeEach('set max slippage', async () => { + const setDefaultMaxSlippageRole = task.interface.getSighash('setDefaultMaxSlippage') + await authorizer + .connect(owner) + .authorize(owner.address, task.address, setDefaultMaxSlippageRole, []) + await task.connect(owner).setDefaultMaxSlippage(slippage) + }) + + const itExecutesTheTaskProperly = (requestedAmount: BigNumberish) => { + it('executes the expected connector', async () => { + const tx = await executeTask(requestedAmount, slippage, data) + + const connectorData = connector.interface.encodeFunctionData('execute', [ + tokenIn.address, + tokenOut.address, + amountIn, + minAmountOut, + data, + ]) + + await assertIndirectEvent(tx, smartVault.interface, 'Executed', { + connector, + data: connectorData, + }) + + await assertIndirectEvent(tx, connector.interface, 'LogExecute', { + tokenIn, + tokenOut, + amountIn, + minAmountOut, + data, + }) + }) + + it('emits an Executed event', async () => { + const tx = await executeTask(requestedAmount, slippage, data) + + await assertIndirectEvent(tx, task.interface, 'Executed') + }) + } + + context('without balance connectors', () => { + const requestedAmount = amountIn + + itExecutesTheTaskProperly(requestedAmount) + + it('does not update any balance connectors', async () => { + const tx = await executeTask(requestedAmount, slippage, data) + + await assertNoEvent(tx, 'BalanceConnectorUpdated') + }) + }) + + context('with balance connectors', () => { + const requestedAmount = 0 + const prevConnectorId = '0x0000000000000000000000000000000000000000000000000000000000000001' + const nextConnectorId = '0x0000000000000000000000000000000000000000000000000000000000000002' + + 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, nextConnectorId) + }) + + 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 in 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, tokenIn.address, amountIn, true) + }) + + itExecutesTheTaskProperly(requestedAmount) + + it('updates the balance connectors properly', async () => { + const tx = await executeTask(requestedAmount, slippage, data) + + await assertIndirectEvent(tx, smartVault.interface, 'BalanceConnectorUpdated', { + id: prevConnectorId, + token: tokenIn.address, + amount: amountIn, + added: false, + }) + + await assertIndirectEvent(tx, smartVault.interface, 'BalanceConnectorUpdated', { + id: nextConnectorId, + token: tokenOut.address, + amount: minAmountOut, + added: true, + }) + }) + }) + }) + + context('when the slippage is above the limit', () => { + const slippage = fp(0.01) + + it('reverts', async () => { + await expect(executeTask(amountIn, slippage, '0x')).to.be.revertedWith('TaskSlippageAboveMax') + }) + }) + }) + + context('when the smart vault balance does not pass the threshold', () => { + const amountIn = thresholdAmountInTokenIn.div(2) + + beforeEach('fund smart vault', async () => { + await tokenIn.mint(smartVault.address, amountIn) + }) + + it('reverts', async () => { + await expect(executeTask(amountIn, 0, '0x')).to.be.revertedWith('TaskTokenThresholdNotMet') + }) + }) + }) + + context('when no off-chain oracle is given', () => { + context('when an on-chain oracle is given', () => { + beforeEach('set price feed', async () => { + const feed = await deployFeedMock(fp(tokenRate), 18) + const setFeedRole = priceOracle.interface.getSighash('setFeed') + await authorizer.connect(owner).authorize(owner.address, priceOracle.address, setFeedRole, []) + await priceOracle.connect(owner).setFeed(tokenIn.address, tokenOut.address, feed.address) + }) + + 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(tokenOut.address, thresholdAmount, 0) + }) + + context('when the smart vault balance passes the threshold', () => { + beforeEach('fund smart vault', async () => { + await tokenIn.mint(smartVault.address, amountIn) + }) + + context('when the slippage is below the limit', () => { + const data = '0xaabb' + const slippage = fp(0.01) + const expectedAmountOut = amountIn.mul(tokenRate) + const minAmountOut = expectedAmountOut.mul(fp(1).sub(slippage)).div(fp(1)) + + beforeEach('set max slippage', async () => { + const setDefaultMaxSlippageRole = task.interface.getSighash('setDefaultMaxSlippage') + await authorizer + .connect(owner) + .authorize(owner.address, task.address, setDefaultMaxSlippageRole, []) + await task.connect(owner).setDefaultMaxSlippage(slippage) + }) + + it('executes the expected connector', async () => { + const tx = await task.call(tokenIn.address, amountIn, slippage, data) + + const connectorData = connector.interface.encodeFunctionData('execute', [ + tokenIn.address, + tokenOut.address, + amountIn, + minAmountOut, + data, + ]) + + await assertIndirectEvent(tx, smartVault.interface, 'Executed', { + connector, + data: connectorData, + }) + + await assertIndirectEvent(tx, connector.interface, 'LogExecute', { + tokenIn, + tokenOut, + amountIn, + minAmountOut, + data, + }) + }) + + it('emits an Executed event', async () => { + const tx = await task.call(tokenIn.address, amountIn, slippage, data) + + await assertIndirectEvent(tx, task.interface, 'Executed') + }) + }) + + context('when the slippage is above the limit', () => { + const slippage = fp(0.01) + + it('reverts', async () => { + await expect(task.call(tokenIn.address, amountIn, slippage, '0x')).to.be.revertedWith( + 'TaskSlippageAboveMax' + ) + }) + }) + }) + + context('when the smart vault balance does not pass the threshold', () => { + const amountIn = thresholdAmountInTokenIn.div(2) + + beforeEach('fund smart vault', async () => { + await tokenIn.mint(smartVault.address, amountIn) + }) + + it('reverts', async () => { + await expect(task.call(tokenIn.address, amountIn, 0, '0x')).to.be.revertedWith( + 'TaskTokenThresholdNotMet' + ) + }) + }) + }) + + context('when no on-chain oracle is given', () => { + it('reverts', async () => { + // TODO: Hardhat does not decode price oracle error properly + await expect(task.call(tokenIn.address, amountIn, 0, '0x')).to.be.reverted + }) + }) + }) + }) + + context('when the token out is not set', () => { + it('reverts', async () => { + await expect(task.call(tokenIn.address, amountIn, 0, '0x')).to.be.revertedWith('TaskTokenOutNotSet') + }) + }) + }) + + context('when the token in is denied', () => { + beforeEach('deny token in', async () => { + const setTokensAcceptanceListRole = task.interface.getSighash('setTokensAcceptanceList') + await authorizer.connect(owner).authorize(owner.address, task.address, setTokensAcceptanceListRole, []) + await task.connect(owner).setTokensAcceptanceList([tokenIn.address], [true]) + }) + + it('reverts', async () => { + await expect(task.call(tokenIn.address, 0, 0, '0x')).to.be.revertedWith('TaskTokenNotAllowed') + }) + }) + }) + + context('when the amount in is zero', () => { + const amountIn = 0 + + it('reverts', async () => { + await expect(task.call(tokenIn.address, amountIn, 0, '0x')).to.be.revertedWith('TaskAmountZero') + }) + }) + }) + + context('when the token in is the zero address', () => { + const tokenIn = ZERO_ADDRESS + + it('reverts', async () => { + await expect(task.call(tokenIn, 0, 0, '0x')).to.be.revertedWith('TaskTokenZero') + }) + }) + }) + + context('when the sender is not authorized', () => { + it('reverts', async () => { + await expect(task.call(ZERO_ADDRESS, 0, 0, '0x')).to.be.revertedWith('AuthSenderNotAllowed') + }) + }) + }) +})