* added initial foundry transformERC20 tests * added foundry tests into CircleCI flow * add verbosity for failing tests in CI * revert wrong CI commands * feat: Foundry, added some more deployments (#558) * Added some more deployments * Rename WETH9 to WETH9V06 * Set to 0.6.x * fix typo * remove commit with bad prettier changes * working bridge Fills through weth transformer * remove unused reference * clean up tests * added working otc fill through transformERC20 in FQT * resolve file imports, add samplers, arbitrumBridgeAdatper, and new FQT version * add extra 'v' for debugging verbosity * add extra 'v' for debugging verbosity in circleci config * remove old traces * refactor rpc's out of foundry.toml and into .env for CI compatibility * remove verbosity from CI command as its now defined in foundry.toml * setup rpc's * ignore foundry artifacts in prettier * change naming in prettierignore * move /samplers to the tests subdirectory, modify remappings to reflext change * one more try 🤞 * change CI steps * remove yarn from CI step * get to the right directory * update foundry before tests * fix tip() deprecation and use deal() * use deal() instead of vm.deal() * try to get foundry to have the right directory structure by updating it * I HATE THIS * remove foundryup * Fix prettier issues * Remove obsoleted import * Use forge native commands to install deps and test and add the --root option * Try using forge with working-directory flag in CI * Use nightly foundry docker image * Update rpc endpoints config in foundry * move tests into /forked and /local * rename tests * add foundry profiles to CI * try to fix CI * 🔧 add foundry local and forked tests to workflow * prettier and lint * revert deps update * remove all samplers and add uniswapV2 sampler to ForkUtils * address jacobs comments * cleanup and comment * prettier and lint * bump contracts-zero-ex version * set func-name-mixedcase to off in solhint for elenas new changes * max line length to warn * add --fix for check-md * Update ci.yml * fix some nitpcks and leftover code * fix inconsistent naming * fix bridge adapter reverts and foundry cache * migrate foundry integration tests to /tests * refactor contract-addresses to use the contract-addresses package style nested json * fix solhint * fix contract linting errors * dont check broken links in libraries * move forge order in gh action for testing * add env instead of vars * try again * fix github actions ordering * update licence and address comments * remove verbosity from foundry.toml * fix contract lint * move back to emitting an event until samplers can be integrated as some chains dont have uniswap as a source * add uniswap v3 sampling code for future use * remove uniswap v3 code as its not used * fix lint Co-authored-by: Noah Khamliche <0xnoah@Noahs-MacBook-Pro-2.local> Co-authored-by: Jacob Evans <jacob@dekz.net> Co-authored-by: elenadimitrova <elena@arenabg.com>
728 lines
29 KiB
Solidity
728 lines
29 KiB
Solidity
// SPDX-License-Identifier: Apache-2.0
|
|
/*
|
|
Copyright 2023 ZeroEx Intl.
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
pragma solidity ^0.6;
|
|
pragma experimental ABIEncoderV2;
|
|
|
|
import "forge-std/Test.sol";
|
|
import "src/features/TransformERC20Feature.sol";
|
|
import "src/external/TransformerDeployer.sol";
|
|
import "src/transformers/WethTransformer.sol";
|
|
import "src/transformers/FillQuoteTransformer.sol";
|
|
import "@0x/contracts-erc20/contracts/src/v06/IEtherTokenV06.sol";
|
|
import "@0x/contracts-erc20/contracts/src/v06/IERC20TokenV06.sol";
|
|
import "src/transformers/bridges/BridgeProtocols.sol";
|
|
import "src/transformers/bridges/EthereumBridgeAdapter.sol";
|
|
import "src/transformers/bridges/PolygonBridgeAdapter.sol";
|
|
import "src/transformers/bridges/BSCBridgeAdapter.sol";
|
|
import "src/transformers/bridges/ArbitrumBridgeAdapter.sol";
|
|
import "src/transformers/bridges/OptimismBridgeAdapter.sol";
|
|
import "src/transformers/bridges/AvalancheBridgeAdapter.sol";
|
|
import "src/transformers/bridges/FantomBridgeAdapter.sol";
|
|
import "src/transformers/bridges/CeloBridgeAdapter.sol";
|
|
import "src/IZeroEx.sol";
|
|
|
|
//contract-addresses/addresses.json interfaces
|
|
struct Transformers {
|
|
address affiliateFeeTransformer;
|
|
address fillQuoteTransformer;
|
|
address payTakerTransformer;
|
|
address positiveSlippageFeeTransformer;
|
|
address wethTransformer;
|
|
}
|
|
struct ContractAddresses {
|
|
address erc20BridgeProxy;
|
|
address erc20BridgeSampler;
|
|
address etherToken;
|
|
address payable exchangeProxy;
|
|
address exchangeProxyFlashWallet;
|
|
address exchangeProxyGovernor;
|
|
address exchangeProxyLiquidityProviderSandbox;
|
|
address exchangeProxyTransformerDeployer;
|
|
address staking;
|
|
address stakingProxy;
|
|
Transformers transformers;
|
|
address zeroExGovernor;
|
|
address zrxToken;
|
|
address zrxTreasury;
|
|
address zrxVault;
|
|
}
|
|
//need to be alphebetized in solidity but not in addresses.json
|
|
struct Addresses {
|
|
address affiliateFeeTransformer;
|
|
address erc20BridgeProxy;
|
|
address erc20BridgeSampler;
|
|
address etherToken;
|
|
address payable exchangeProxy;
|
|
address exchangeProxyFlashWallet;
|
|
address exchangeProxyGovernor;
|
|
address exchangeProxyLiquidityProviderSandbox;
|
|
address exchangeProxyTransformerDeployer;
|
|
address fillQuoteTransformer;
|
|
address payTakerTransformer;
|
|
address positiveSlippageFeeTransformer;
|
|
address staking;
|
|
address stakingProxy;
|
|
address wethTransformer;
|
|
address zeroExGovernor;
|
|
address zrxToken;
|
|
address zrxTreasury;
|
|
address zrxVault;
|
|
}
|
|
|
|
struct TokenAddresses {
|
|
IERC20TokenV06 DAI;
|
|
IERC20TokenV06 USDC;
|
|
IERC20TokenV06 USDT;
|
|
IEtherTokenV06 WrappedNativeToken;
|
|
}
|
|
|
|
struct LiquiditySources {
|
|
address UniswapV2Router;
|
|
address UniswapV3Router;
|
|
}
|
|
|
|
interface IFQT {
|
|
function bridgeAdapter() external returns (address);
|
|
}
|
|
|
|
interface IUniswapV2Router01 {
|
|
function getAmountsOut(uint256 amountIn, address[] calldata path) external view returns (uint256[] memory amounts);
|
|
|
|
function getAmountsIn(uint256 amountOut, address[] calldata path) external view returns (uint256[] memory amounts);
|
|
}
|
|
|
|
interface IUniswapV3QuoterV2 {
|
|
function factory() external view returns (IUniswapV3Factory factory);
|
|
|
|
// @notice Returns the amount out received for a given exact input swap without executing the swap
|
|
// @param path The path of the swap, i.e. each token pair and the pool fee
|
|
// @param amountIn The amount of the first token to swap
|
|
// @return amountOut The amount of the last token that would be received
|
|
// @return sqrtPriceX96AfterList List of the sqrt price after the swap for each pool in the path
|
|
// @return initializedTicksCrossedList List of the initialized ticks that the swap crossed for each pool in the path
|
|
// @return gasEstimate The estimate of the gas that the swap consumes
|
|
function quoteExactInput(
|
|
bytes memory path,
|
|
uint256 amountIn
|
|
)
|
|
external
|
|
returns (
|
|
uint256 amountOut,
|
|
uint160[] memory sqrtPriceX96AfterList,
|
|
uint32[] memory initializedTicksCrossedList,
|
|
uint256 gasEstimate
|
|
);
|
|
|
|
// @notice Returns the amount in required for a given exact output swap without executing the swap
|
|
// @param path The path of the swap, i.e. each token pair and the pool fee. Path must be provided in reverse order
|
|
// @param amountOut The amount of the last token to receive
|
|
// @return amountIn The amount of first token required to be paid
|
|
// @return sqrtPriceX96AfterList List of the sqrt price after the swap for each pool in the path
|
|
// @return initializedTicksCrossedList List of the initialized ticks that the swap crossed for each pool in the path
|
|
// @return gasEstimate The estimate of the gas that the swap consumes
|
|
function quoteExactOutput(
|
|
bytes memory path,
|
|
uint256 amountOut
|
|
)
|
|
external
|
|
returns (
|
|
uint256 amountIn,
|
|
uint160[] memory sqrtPriceX96AfterList,
|
|
uint32[] memory initializedTicksCrossedList,
|
|
uint256 gasEstimate
|
|
);
|
|
}
|
|
|
|
interface IUniswapV3Factory {
|
|
function getPool(IERC20TokenV06 a, IERC20TokenV06 b, uint24 fee) external view returns (IUniswapV3Pool pool);
|
|
}
|
|
|
|
interface IUniswapV3Pool {
|
|
function token0() external view returns (IERC20TokenV06);
|
|
|
|
function token1() external view returns (IERC20TokenV06);
|
|
|
|
function fee() external view returns (uint24);
|
|
}
|
|
|
|
contract ForkUtils is Test {
|
|
using stdJson for string;
|
|
|
|
string json;
|
|
|
|
//forked providers for each chain
|
|
mapping(string => uint256) public forkIds;
|
|
|
|
IZeroEx IZERO_EX;
|
|
|
|
IBridgeAdapter bridgeAdapter;
|
|
FillQuoteTransformer fillQuoteTransformer;
|
|
|
|
TokenAddresses tokens;
|
|
Transformers transformers;
|
|
ContractAddresses addresses;
|
|
LiquiditySources sources;
|
|
|
|
uint256 forkBlock = 15_000_000;
|
|
|
|
string[] chains = ["mainnet", "bsc", "polygon", "avalanche", "fantom", "optimism", "arbitrum"];
|
|
string[] indexChainIds = [".1", ".56", ".137", ".43114", ".250", ".10", ".42161"];
|
|
string[] chainIds = ["1", "56", "137", "43114", "250", "10", "42161"];
|
|
uint256[] chainId = [1, 56, 137, 43114, 250, 10, 42161];
|
|
|
|
//special fork block number for fantom since it produces blocks faster and more frequently
|
|
uint256[] blockNumber = [forkBlock, forkBlock, 33447149, forkBlock, 32000000, forkBlock, forkBlock];
|
|
|
|
string addressesJson;
|
|
string tokensJson;
|
|
string sourcesJson;
|
|
|
|
//utility mapping to get chainId by name
|
|
mapping(string => string) public chainsByChainId;
|
|
//utility mapping to get indexingChainId by Chain
|
|
mapping(string => string) public indexChainsByChain;
|
|
|
|
function createForks() public returns (uint256[] memory) {
|
|
for (uint256 i = 0; i < chains.length; i++) {
|
|
forkIds[chains[i]] = vm.createFork(vm.rpcUrl(chains[i]), blockNumber[i]);
|
|
}
|
|
}
|
|
|
|
//gets a dummy signer to be used for an OTC order
|
|
function getSigner() public returns (address, uint) {
|
|
string memory mnemonic = "test test test test test test test test test test test junk";
|
|
uint256 privateKey = vm.deriveKey(mnemonic, 0);
|
|
|
|
vm.label(vm.addr(privateKey), "zeroEx/MarketMaker");
|
|
return (vm.addr(privateKey), privateKey);
|
|
}
|
|
|
|
//read the uniswapV2 router addresses from file
|
|
function readLiquiditySourceAddresses() internal returns (string memory) {
|
|
string memory root = vm.projectRoot();
|
|
string memory path = string(abi.encodePacked(root, "/", "tests/addresses/SourceAddresses.json"));
|
|
sourcesJson = vm.readFile(path);
|
|
return vm.readFile(path);
|
|
}
|
|
|
|
//retrieve the uniswapV2 router addresses
|
|
function getLiquiditySourceAddresses(uint index) public returns (LiquiditySources memory sources) {
|
|
readLiquiditySourceAddresses();
|
|
bytes memory liquiditySources = sourcesJson.parseRaw(indexChainIds[index]);
|
|
return abi.decode(liquiditySources, (LiquiditySources));
|
|
}
|
|
|
|
function readAddresses() internal returns (string memory) {
|
|
string memory root = vm.projectRoot();
|
|
string memory path = string(abi.encodePacked(root, "/", "tests/addresses/ContractAddresses.json"));
|
|
addressesJson = vm.readFile(path);
|
|
return vm.readFile(path);
|
|
}
|
|
|
|
//retrieve the 0x Protocol contract addresses from addresses.json
|
|
function getContractAddresses(uint index) public returns (ContractAddresses memory addresses) {
|
|
readAddresses();
|
|
bytes memory contractAddresses = addressesJson.parseRaw(indexChainIds[index]);
|
|
return abi.decode(contractAddresses, (ContractAddresses));
|
|
}
|
|
|
|
function readTokens() internal returns (string memory) {
|
|
string memory root = vm.projectRoot();
|
|
string memory path = string(abi.encodePacked(root, "/", "tests/addresses/TokenAddresses.json"));
|
|
tokensJson = vm.readFile(path);
|
|
return vm.readFile(path);
|
|
}
|
|
|
|
//retrieve our tokenList from TokenAddresses.json
|
|
function getTokens(uint index) public returns (TokenAddresses memory addresses) {
|
|
readTokens();
|
|
bytes memory chainTokens = tokensJson.parseRaw(indexChainIds[index]);
|
|
return abi.decode(chainTokens, (TokenAddresses));
|
|
}
|
|
|
|
//creates the appropriate bridge adapter based on what chain the tests are currently executing on.
|
|
function createBridgeAdapter(IEtherTokenV06 weth) public returns (IBridgeAdapter bridgeAdapter) {
|
|
uint chainId;
|
|
|
|
assembly {
|
|
chainId := chainid()
|
|
}
|
|
if (chainId == 1) {
|
|
return IBridgeAdapter(new EthereumBridgeAdapter(weth));
|
|
} else if (chainId == 56) {
|
|
return IBridgeAdapter(new BSCBridgeAdapter(weth));
|
|
} else if (chainId == 137) {
|
|
return IBridgeAdapter(new PolygonBridgeAdapter(weth));
|
|
} else if (chainId == 43114) {
|
|
return IBridgeAdapter(new AvalancheBridgeAdapter(weth));
|
|
} else if (chainId == 250) {
|
|
return IBridgeAdapter(new FantomBridgeAdapter(weth));
|
|
} else if (chainId == 10) {
|
|
return IBridgeAdapter(new OptimismBridgeAdapter(weth));
|
|
} else if (chainId == 42161) {
|
|
return IBridgeAdapter(new ArbitrumBridgeAdapter(weth));
|
|
} else {
|
|
//ERROR: chainId not mapped
|
|
revert("ChainId not supported");
|
|
}
|
|
}
|
|
|
|
//label the addresses that are read from the various .json files
|
|
function labelAddresses(
|
|
string memory chainName,
|
|
string memory chainId,
|
|
TokenAddresses memory tokens,
|
|
ContractAddresses memory addresses,
|
|
LiquiditySources memory sources
|
|
) public {
|
|
log_named_string(" Using contract addresses on chain", chainName);
|
|
vm.label(addresses.transformers.affiliateFeeTransformer, "zeroEx/affiliateFeeTransformer");
|
|
vm.label(addresses.erc20BridgeProxy, "zeroEx/erc20BridgeProxy");
|
|
vm.label(addresses.erc20BridgeSampler, "zeroEx/erc20BridgeSampler");
|
|
vm.label(addresses.etherToken, "zeroEx/etherToken");
|
|
vm.label(addresses.exchangeProxy, "zeroEx/exchangeProxy");
|
|
vm.label(addresses.exchangeProxyFlashWallet, "zeroEx/exchangeProxyFlashWallet");
|
|
vm.label(addresses.exchangeProxyGovernor, "zeroEx/exchangeProxyGovernor");
|
|
vm.label(addresses.exchangeProxyLiquidityProviderSandbox, "zeroEx/exchangeProxyLiquidityProviderSandbox");
|
|
vm.label(addresses.exchangeProxyTransformerDeployer, "zeroEx/exchangeProxyTransformerDeployer");
|
|
vm.label(addresses.transformers.fillQuoteTransformer, "zeroEx/fillQuoteTransformer");
|
|
vm.label(addresses.transformers.payTakerTransformer, "zeroEx/payTakerTransformer");
|
|
vm.label(addresses.transformers.positiveSlippageFeeTransformer, "zeroEx/positiveSlippageFeeTransformer");
|
|
vm.label(addresses.staking, "zeroEx/staking");
|
|
vm.label(addresses.stakingProxy, "zeroEx/stakingProxy");
|
|
vm.label(addresses.transformers.wethTransformer, "zeroEx/wethTransformer");
|
|
vm.label(addresses.zeroExGovernor, "zeroEx/zeroExGovernor");
|
|
vm.label(addresses.zrxToken, "zeroEx/zrxToken");
|
|
vm.label(addresses.zrxTreasury, "zeroEx/zrxTreasury");
|
|
vm.label(addresses.zrxVault, "zeroEx/zrxVault");
|
|
vm.label(address(tokens.WrappedNativeToken), "WrappedNativeToken");
|
|
vm.label(address(tokens.USDT), "USDT");
|
|
vm.label(address(tokens.USDC), "USDC");
|
|
vm.label(address(tokens.DAI), "DAI");
|
|
vm.label(address(sources.UniswapV2Router), "UniswapRouter");
|
|
}
|
|
|
|
//deploy a new FillQuoteTransformer
|
|
//executes in the context of the transformerDeployer
|
|
function createNewFQT(
|
|
IEtherTokenV06 wrappedNativeToken,
|
|
address payable exchangeProxy,
|
|
address transformerDeployer
|
|
) public {
|
|
vm.startPrank(transformerDeployer);
|
|
// deploy a new instance of the bridge adapter from the transformerDeployer
|
|
bridgeAdapter = createBridgeAdapter(IEtherTokenV06(wrappedNativeToken));
|
|
// deploy a new instance of the fill quote transformer from the transformerDeployer
|
|
fillQuoteTransformer = new FillQuoteTransformer(IBridgeAdapter(bridgeAdapter), IZeroEx(exchangeProxy));
|
|
vm.label(address(fillQuoteTransformer), "zeroEx/FillQuoteTransformer");
|
|
vm.stopPrank();
|
|
}
|
|
|
|
//utility function to be called in the `setUp()` function of a test
|
|
//creates a fork and retrieves the correct addresses based on chainId
|
|
function _setup() public {
|
|
//get our addresses.json file that defines contract addresses for each chain we are currently deployed on
|
|
string memory root = vm.projectRoot();
|
|
string memory path = string(abi.encodePacked(root, "/", "tests/addresses/ContractAddresses.json"));
|
|
json = vm.readFile(path);
|
|
createForks();
|
|
|
|
for (uint256 i = 0; i < chains.length; i++) {
|
|
chainsByChainId[chains[i]] = chainIds[i];
|
|
indexChainsByChain[chains[i]] = indexChainIds[i];
|
|
bytes memory details = json.parseRaw(indexChainIds[i]);
|
|
addresses = abi.decode(details, (ContractAddresses));
|
|
}
|
|
}
|
|
|
|
/// @dev Sample sell quotes from UniswapV2.
|
|
/// @param router Router to look up tokens and amounts
|
|
/// @param path Token route. Should be takerToken -> makerToken.
|
|
/// @param takerTokenAmounts Taker token sell amount for each sample.
|
|
/// @return makerTokenAmounts Taker amounts sold at each maker token
|
|
/// amount.
|
|
function sampleSellsFromUniswapV2(
|
|
address router,
|
|
address[] memory path,
|
|
uint256[] memory takerTokenAmounts
|
|
) public view returns (uint256[] memory makerTokenAmounts) {
|
|
uint256 numSamples = takerTokenAmounts.length;
|
|
makerTokenAmounts = new uint256[](numSamples);
|
|
for (uint256 i = 0; i < numSamples; i++) {
|
|
try IUniswapV2Router01(router).getAmountsOut(takerTokenAmounts[i], path) returns (
|
|
uint256[] memory amounts
|
|
) {
|
|
makerTokenAmounts[i] = amounts[path.length - 1];
|
|
// Break early if there are 0 amounts
|
|
if (makerTokenAmounts[i] == 0) {
|
|
break;
|
|
}
|
|
} catch (bytes memory) {
|
|
// Swallow failures, leaving all results as zero.
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// @dev Sample buy quotes from UniswapV2.
|
|
/// @param router Router to look up tokens and amounts
|
|
/// @param path Token route. Should be takerToken -> makerToken.
|
|
/// @param makerTokenAmounts Maker token buy amount for each sample.
|
|
/// @return takerTokenAmounts Taker amounts sold at each maker token
|
|
/// amount.
|
|
function sampleBuysFromUniswapV2(
|
|
address router,
|
|
address[] memory path,
|
|
uint256[] memory makerTokenAmounts
|
|
) public view returns (uint256[] memory takerTokenAmounts) {
|
|
uint256 numSamples = makerTokenAmounts.length;
|
|
takerTokenAmounts = new uint256[](numSamples);
|
|
for (uint256 i = 0; i < numSamples; i++) {
|
|
try IUniswapV2Router01(router).getAmountsIn(makerTokenAmounts[i], path) returns (uint256[] memory amounts) {
|
|
takerTokenAmounts[i] = amounts[0];
|
|
// Break early if there are 0 amounts
|
|
if (takerTokenAmounts[i] == 0) {
|
|
break;
|
|
}
|
|
} catch (bytes memory) {
|
|
// Swallow failures, leaving all results as zero.
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// @dev Sample sell quotes from UniswapV3.
|
|
/// @param quoter UniswapV3 Quoter contract.
|
|
/// @param path Token route. Should be takerToken -> makerToken (at most two hops).
|
|
/// @param takerTokenAmounts Taker token sell amount for each sample.
|
|
/// @return uniswapPaths The encoded uniswap path for each sample.
|
|
/// @return uniswapGasUsed Estimated amount of gas used
|
|
/// @return makerTokenAmounts Maker amounts bought at each taker token amount.
|
|
function sampleSellsFromUniswapV3(
|
|
IUniswapV3QuoterV2 quoter,
|
|
IERC20TokenV06[] memory path,
|
|
uint256[] memory takerTokenAmounts
|
|
)
|
|
public
|
|
returns (bytes[] memory uniswapPaths, uint256[] memory uniswapGasUsed, uint256[] memory makerTokenAmounts)
|
|
{
|
|
IUniswapV3Pool[][] memory poolPaths = _getPoolPaths(
|
|
quoter,
|
|
path,
|
|
takerTokenAmounts[takerTokenAmounts.length - 1]
|
|
);
|
|
|
|
makerTokenAmounts = new uint256[](takerTokenAmounts.length);
|
|
uniswapPaths = new bytes[](takerTokenAmounts.length);
|
|
uniswapGasUsed = new uint256[](takerTokenAmounts.length);
|
|
|
|
for (uint256 i = 0; i < takerTokenAmounts.length; ++i) {
|
|
// Pick the best result from the pool paths.
|
|
uint256 topBuyAmount = 0;
|
|
for (uint256 j = 0; j < poolPaths.length; ++j) {
|
|
if (!isValidPoolPath(poolPaths[j])) {
|
|
continue;
|
|
}
|
|
|
|
bytes memory uniswapPath = _toUniswapPath(path, poolPaths[j]);
|
|
try quoter.quoteExactInput(uniswapPath, takerTokenAmounts[i]) returns (
|
|
uint256 buyAmount,
|
|
uint160[] memory /* sqrtPriceX96AfterList */,
|
|
uint32[] memory /* initializedTicksCrossedList */,
|
|
uint256 gasUsed
|
|
) {
|
|
if (topBuyAmount <= buyAmount) {
|
|
topBuyAmount = buyAmount;
|
|
uniswapPaths[i] = uniswapPath;
|
|
uniswapGasUsed[i] = gasUsed;
|
|
}
|
|
} catch {}
|
|
}
|
|
// Break early if we can't complete the sells.
|
|
if (topBuyAmount == 0) {
|
|
// HACK(kimpers): To avoid too many local variables, paths and gas used is set directly in the loop
|
|
// then reset if no valid valid quote was found
|
|
uniswapPaths[i] = "";
|
|
uniswapGasUsed[i] = 0;
|
|
break;
|
|
}
|
|
makerTokenAmounts[i] = topBuyAmount;
|
|
}
|
|
}
|
|
|
|
/// @dev Sample buy quotes from UniswapV3.
|
|
/// @param quoter UniswapV3 Quoter contract.
|
|
/// @param path Token route. Should be takerToken -> makerToken (at most two hops).
|
|
/// @param makerTokenAmounts Maker token buy amount for each sample.
|
|
/// @return uniswapPaths The encoded uniswap path for each sample.
|
|
/// @return uniswapGasUsed Estimated amount of gas used
|
|
/// @return takerTokenAmounts Taker amounts sold at each maker token amount.
|
|
function sampleBuysFromUniswapV3(
|
|
IUniswapV3QuoterV2 quoter,
|
|
IERC20TokenV06[] memory path,
|
|
uint256[] memory makerTokenAmounts
|
|
)
|
|
public
|
|
returns (bytes[] memory uniswapPaths, uint256[] memory uniswapGasUsed, uint256[] memory takerTokenAmounts)
|
|
{
|
|
IERC20TokenV06[] memory reversedPath = _reverseTokenPath(path);
|
|
IUniswapV3Pool[][] memory poolPaths = _getPoolPaths(
|
|
quoter,
|
|
reversedPath,
|
|
makerTokenAmounts[makerTokenAmounts.length - 1]
|
|
);
|
|
|
|
takerTokenAmounts = new uint256[](makerTokenAmounts.length);
|
|
uniswapPaths = new bytes[](makerTokenAmounts.length);
|
|
uniswapGasUsed = new uint256[](makerTokenAmounts.length);
|
|
|
|
for (uint256 i = 0; i < makerTokenAmounts.length; ++i) {
|
|
// Pick the best result from the pool paths.
|
|
uint256 topSellAmount = 0;
|
|
for (uint256 j = 0; j < poolPaths.length; ++j) {
|
|
if (!isValidPoolPath(poolPaths[j])) {
|
|
continue;
|
|
}
|
|
|
|
// quoter requires path to be reversed for buys.
|
|
bytes memory uniswapPath = _toUniswapPath(reversedPath, poolPaths[j]);
|
|
try quoter.quoteExactOutput(uniswapPath, makerTokenAmounts[i]) returns (
|
|
uint256 sellAmount,
|
|
uint160[] memory /* sqrtPriceX96AfterList */,
|
|
uint32[] memory /* initializedTicksCrossedList */,
|
|
uint256 gasUsed
|
|
) {
|
|
if (topSellAmount == 0 || topSellAmount >= sellAmount) {
|
|
topSellAmount = sellAmount;
|
|
// But the output path should still be encoded for sells.
|
|
uniswapPaths[i] = _toUniswapPath(path, _reversePoolPath(poolPaths[j]));
|
|
uniswapGasUsed[i] = gasUsed;
|
|
}
|
|
} catch {}
|
|
}
|
|
// Break early if we can't complete the buys.
|
|
if (topSellAmount == 0) {
|
|
// HACK(kimpers): To avoid too many local variables, paths and gas used is set directly in the loop
|
|
// then reset if no valid valid quote was found
|
|
uniswapPaths[i] = "";
|
|
uniswapGasUsed[i] = 0;
|
|
break;
|
|
}
|
|
takerTokenAmounts[i] = topSellAmount;
|
|
}
|
|
}
|
|
|
|
function _getPoolPaths(
|
|
IUniswapV3QuoterV2 quoter,
|
|
IERC20TokenV06[] memory path,
|
|
uint256 inputAmount
|
|
) private returns (IUniswapV3Pool[][] memory poolPaths) {
|
|
if (path.length == 2) {
|
|
return _getPoolPathSingleHop(quoter, path, inputAmount);
|
|
}
|
|
if (path.length == 3) {
|
|
return _getPoolPathTwoHop(quoter, path, inputAmount);
|
|
}
|
|
revert("UniswapV3Sampler/unsupported token path length");
|
|
}
|
|
|
|
function _getPoolPathSingleHop(
|
|
IUniswapV3QuoterV2 quoter,
|
|
IERC20TokenV06[] memory path,
|
|
uint256 inputAmount
|
|
) public returns (IUniswapV3Pool[][] memory poolPaths) {
|
|
poolPaths = new IUniswapV3Pool[][](2);
|
|
(IUniswapV3Pool[2] memory topPools, ) = _getTopTwoPools(
|
|
quoter,
|
|
quoter.factory(),
|
|
path[0],
|
|
path[1],
|
|
inputAmount
|
|
);
|
|
|
|
uint256 pathCount = 0;
|
|
for (uint256 i = 0; i < 2; i++) {
|
|
IUniswapV3Pool topPool = topPools[i];
|
|
poolPaths[pathCount] = new IUniswapV3Pool[](1);
|
|
poolPaths[pathCount][0] = topPool;
|
|
pathCount++;
|
|
}
|
|
}
|
|
|
|
function _getPoolPathTwoHop(
|
|
IUniswapV3QuoterV2 quoter,
|
|
IERC20TokenV06[] memory path,
|
|
uint256 inputAmount
|
|
) private returns (IUniswapV3Pool[][] memory poolPaths) {
|
|
IUniswapV3Factory factory = quoter.factory();
|
|
poolPaths = new IUniswapV3Pool[][](4);
|
|
(IUniswapV3Pool[2] memory firstHopTopPools, uint256[2] memory firstHopAmounts) = _getTopTwoPools(
|
|
quoter,
|
|
factory,
|
|
path[0],
|
|
path[1],
|
|
inputAmount
|
|
);
|
|
(IUniswapV3Pool[2] memory secondHopTopPools, ) = _getTopTwoPools(
|
|
quoter,
|
|
factory,
|
|
path[1],
|
|
path[2],
|
|
firstHopAmounts[0]
|
|
);
|
|
|
|
uint256 pathCount = 0;
|
|
for (uint256 i = 0; i < 2; i++) {
|
|
for (uint256 j = 0; j < 2; j++) {
|
|
poolPaths[pathCount] = new IUniswapV3Pool[](2);
|
|
IUniswapV3Pool[] memory currentPath = poolPaths[pathCount];
|
|
currentPath[0] = firstHopTopPools[i];
|
|
currentPath[1] = secondHopTopPools[j];
|
|
pathCount++;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// @dev Returns top 0-2 pools and corresponding output amounts based on swaping `inputAmount`.
|
|
/// Addresses in `topPools` can be zero addresses when there are pool isn't available.
|
|
function _getTopTwoPools(
|
|
IUniswapV3QuoterV2 quoter,
|
|
IUniswapV3Factory factory,
|
|
IERC20TokenV06 inputToken,
|
|
IERC20TokenV06 outputToken,
|
|
uint256 inputAmount
|
|
) private returns (IUniswapV3Pool[2] memory topPools, uint256[2] memory outputAmounts) {
|
|
IERC20TokenV06[] memory path = new IERC20TokenV06[](2);
|
|
path[0] = inputToken;
|
|
path[1] = outputToken;
|
|
|
|
uint24[4] memory validPoolFees = [uint24(0.0001e6), uint24(0.0005e6), uint24(0.003e6), uint24(0.01e6)];
|
|
for (uint256 i = 0; i < validPoolFees.length; ++i) {
|
|
IUniswapV3Pool pool = factory.getPool(inputToken, outputToken, validPoolFees[i]);
|
|
if (!_isValidPool(pool)) {
|
|
continue;
|
|
}
|
|
|
|
IUniswapV3Pool[] memory poolPath = new IUniswapV3Pool[](1);
|
|
poolPath[0] = pool;
|
|
bytes memory uniswapPath = _toUniswapPath(path, poolPath);
|
|
try quoter.quoteExactInput(uniswapPath, inputAmount) returns (
|
|
uint256 outputAmount,
|
|
uint160[] memory /* sqrtPriceX96AfterList */,
|
|
uint32[] memory /* initializedTicksCrossedList */,
|
|
uint256 /* gasUsed */
|
|
) {
|
|
// Keeping track of the top 2 pools.
|
|
if (outputAmount > outputAmounts[0]) {
|
|
outputAmounts[1] = outputAmounts[0];
|
|
topPools[1] = topPools[0];
|
|
outputAmounts[0] = outputAmount;
|
|
topPools[0] = pool;
|
|
} else if (outputAmount > outputAmounts[1]) {
|
|
outputAmounts[1] = outputAmount;
|
|
topPools[1] = pool;
|
|
}
|
|
} catch {}
|
|
}
|
|
}
|
|
|
|
function _reverseTokenPath(
|
|
IERC20TokenV06[] memory tokenPath
|
|
) private pure returns (IERC20TokenV06[] memory reversed) {
|
|
reversed = new IERC20TokenV06[](tokenPath.length);
|
|
for (uint256 i = 0; i < tokenPath.length; ++i) {
|
|
reversed[i] = tokenPath[tokenPath.length - i - 1];
|
|
}
|
|
}
|
|
|
|
function _reversePoolPath(
|
|
IUniswapV3Pool[] memory poolPath
|
|
) private pure returns (IUniswapV3Pool[] memory reversed) {
|
|
reversed = new IUniswapV3Pool[](poolPath.length);
|
|
for (uint256 i = 0; i < poolPath.length; ++i) {
|
|
reversed[i] = poolPath[poolPath.length - i - 1];
|
|
}
|
|
}
|
|
|
|
function _isValidPool(IUniswapV3Pool pool) private view returns (bool isValid) {
|
|
// Check if it has been deployed.
|
|
{
|
|
uint256 codeSize;
|
|
assembly {
|
|
codeSize := extcodesize(pool)
|
|
}
|
|
if (codeSize == 0) {
|
|
return false;
|
|
}
|
|
}
|
|
// Must have a balance of both tokens.
|
|
if (pool.token0().balanceOf(address(pool)) == 0) {
|
|
return false;
|
|
}
|
|
if (pool.token1().balanceOf(address(pool)) == 0) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function isValidPoolPath(IUniswapV3Pool[] memory poolPaths) private pure returns (bool) {
|
|
for (uint256 i = 0; i < poolPaths.length; i++) {
|
|
if (address(poolPaths[i]) == address(0)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function _toUniswapPath(
|
|
IERC20TokenV06[] memory tokenPath,
|
|
IUniswapV3Pool[] memory poolPath
|
|
) private view returns (bytes memory uniswapPath) {
|
|
require(
|
|
tokenPath.length >= 2 && tokenPath.length == poolPath.length + 1,
|
|
"UniswapV3Sampler/invalid path lengths"
|
|
);
|
|
// Uniswap paths are tightly packed as:
|
|
// [token0, token0token1PairFee, token1, token1Token2PairFee, token2, ...]
|
|
uniswapPath = new bytes(tokenPath.length * 20 + poolPath.length * 3);
|
|
uint256 o;
|
|
assembly {
|
|
o := add(uniswapPath, 32)
|
|
}
|
|
for (uint256 i = 0; i < tokenPath.length; ++i) {
|
|
if (i > 0) {
|
|
uint24 poolFee = poolPath[i - 1].fee();
|
|
assembly {
|
|
mstore(o, shl(232, poolFee))
|
|
o := add(o, 3)
|
|
}
|
|
}
|
|
IERC20TokenV06 token = tokenPath[i];
|
|
assembly {
|
|
mstore(o, shl(96, token))
|
|
o := add(o, 20)
|
|
}
|
|
}
|
|
}
|
|
|
|
modifier onlyForked() {
|
|
if (block.number >= 15000000) {
|
|
_;
|
|
} else {
|
|
revert("Requires fork mode");
|
|
}
|
|
}
|
|
}
|