From d1933201ca591f5ae154598163a0a36c0e354025 Mon Sep 17 00:00:00 2001 From: rayeaster Date: Thu, 14 Jul 2022 17:34:12 +0800 Subject: [PATCH 01/53] gas optimization for univ3 & balancer --- contracts/OnChainPricingMainnet.sol | 100 ++++++++++++++---- interfaces/balancer/IBalancerV2Vault.sol | 4 + .../balancer/IBalancerV2WeightedPool.sol | 7 ++ tests/aura_processor/test_emit_aura.py | 1 - tests/aura_processor/test_ragequit_aura.py | 1 - tests/bribes_processor/test_emit.py | 2 - tests/bribes_processor/test_ragequit.py | 1 - tests/conftest.py | 5 + tests/on_chain_pricer/test_balancer_pricer.py | 59 +++++++++-- tests/on_chain_pricer/test_univ3_pricer.py | 10 +- 10 files changed, 148 insertions(+), 42 deletions(-) create mode 100644 interfaces/balancer/IBalancerV2WeightedPool.sol diff --git a/contracts/OnChainPricingMainnet.sol b/contracts/OnChainPricingMainnet.sol index 13db1e9..c5d3db3 100644 --- a/contracts/OnChainPricingMainnet.sol +++ b/contracts/OnChainPricingMainnet.sol @@ -12,6 +12,7 @@ import "../interfaces/uniswap/IUniswapRouterV2.sol"; import "../interfaces/uniswap/IV3Pool.sol"; import "../interfaces/uniswap/IV3Quoter.sol"; import "../interfaces/balancer/IBalancerV2Vault.sol"; +import "../interfaces/balancer/IBalancerV2WeightedPool.sol"; import "../interfaces/curve/ICurveRouter.sol"; import "../interfaces/curve/ICurvePool.sol"; @@ -58,7 +59,7 @@ contract OnChainPricingMainnet { address public constant UNIV3_QUOTER = 0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6; bytes32 public constant UNIV3_POOL_INIT_CODE_HASH = 0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54; address public constant UNIV3_FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984; - uint24[4] univ3_fees = [uint24(100), 500, 3000, 10000]; + uint24[3] univ3_fees = [uint24(100), 500, 3000];//, 10000]; //skip last fee for exotic tokens // BalancerV2 Vault address public constant BALANCERV2_VAULT = 0xBA12222222228d8Ba445958a75a0704d566BF2C8; @@ -175,12 +176,12 @@ contract OnChainPricingMainnet { quotes[3] = Quote(SwapType.UNIV3, getUniV3Price(tokenIn, amountIn, tokenOut), dummyPools, dummyPoolFees); - quotes[4] = Quote(SwapType.BALANCER, getBalancerPrice(tokenIn, amountIn, tokenOut), dummyPools, dummyPoolFees); + quotes[4] = Quote(SwapType.BALANCER, getBalancerPriceAnalytically(tokenIn, amountIn, tokenOut), dummyPools, dummyPoolFees); if(!wethInvolved){ quotes[5] = Quote(SwapType.UNIV3WITHWETH, getUniV3PriceWithConnector(tokenIn, amountIn, tokenOut, WETH), dummyPools, dummyPoolFees); - quotes[6] = Quote(SwapType.BALANCERWITHWETH, getBalancerPriceWithConnector(tokenIn, amountIn, tokenOut, WETH), dummyPools, dummyPoolFees); + quotes[6] = Quote(SwapType.BALANCERWITHWETH, getBalancerPriceWithConnectorAnalytically(tokenIn, amountIn, tokenOut, WETH), dummyPools, dummyPoolFees); } // Because this is a generalized contract, it is best to just loop, @@ -240,9 +241,9 @@ contract OnChainPricingMainnet { //filter out disqualified pools to save gas on quoter swap query uint256 rate = _getUniV3Rate(token0, token1, univ3_fees[i], token0Price, amountIn); if (rate > 0){ - uint256 quote = _getUniV3QuoterQuery(tokenIn, tokenOut, univ3_fees[i], amountIn); - if (quote > quoteRate){ - quoteRate = quote; + //uint256 quote = _getUniV3QuoterQuery(tokenIn, tokenOut, univ3_fees[i], amountIn);// skip this "call and revert" gas-consuming method + if (rate > quoteRate){ + quoteRate = rate; } } @@ -281,46 +282,50 @@ contract OnChainPricingMainnet { /// @return the current price in V3 for it function _getUniV3Rate(address token0, address token1, uint24 fee, bool token0Price, uint256 amountIn) internal view returns (uint256) { - // heuristic check0: ensure the pool [exist] and properly initiated + // heuristic check0: ensure the pool [exist] and properly initiated with valid in-range liquidity address pool = _getUniV3PoolAddress(token0, token1, fee); if (!pool.isContract() || IUniswapV3Pool(pool).liquidity() == 0) { return 0; } + uint256 _t0Balance = IERC20(token0).balanceOf(pool); + uint256 _t1Balance = IERC20(token1).balanceOf(pool); + // heuristic check1: ensure the pool tokenIn reserve makes sense in terms of [amountIn] - if (IERC20(token0Price? token0 : token1).balanceOf(pool) <= amountIn){ + if ((token0Price? _t0Balance : _t1Balance) <= amountIn){ return 0; } - + + uint256 _t0Decimals = 10 ** IERC20Metadata(token0).decimals(); + uint256 _t1Decimals = 10 ** IERC20Metadata(token1).decimals(); + // heuristic check2: ensure the pool tokenOut reserve makes sense in terms of the [amountOutput based on slot0 price] - uint256 rate = _queryUniV3PriceWithSlot(token0, token1, pool, token0Price); - uint256 amountOutput = rate * amountIn * (10 ** IERC20Metadata(token0Price? token1 : token0).decimals()) / (10 ** IERC20Metadata(token0Price? token0 : token1).decimals()) / 1e18; - if (IERC20(token0Price? token1 : token0).balanceOf(pool) <= amountOutput){ + uint256 rate = _queryUniV3PriceWithSlot(token0, token1, pool, token0Price, _t0Decimals, _t1Decimals); + uint256 amountOutput = rate * amountIn * (token0Price? _t1Decimals : _t0Decimals) / (token0Price? _t0Decimals : _t1Decimals) / 1e18; + if ((token0Price? _t1Balance : _t0Balance) <= amountOutput){ return 0; } // heuristic check3: ensure the pool [reserve comparison is consistent with the slot0 price comparison], i.e., asset in less amount should be more expensive in AMM pool bool token0MoreExpensive = _compareUniV3Tokens(token0Price, rate); - bool token0MoreReserved = _compareUniV3TokenReserves(token0, token1, pool); + bool token0MoreReserved = _compareUniV3TokenReserves(_t0Balance, _t1Balance, _t0Decimals, _t1Decimals); if (token0MoreExpensive == token0MoreReserved){ return 0; } - return rate; + return amountOutput; } /// @dev query current price from V3 pool interface(slot0) with given pool & token0 & token1 /// @dev and indicator if token0 pricing required (token1/token0 e.g., token0 -> token1) /// @return the price of required token scaled with 1e18 - function _queryUniV3PriceWithSlot(address token0, address token1, address pool, bool token0Price) internal view returns (uint256) { + function _queryUniV3PriceWithSlot(address token0, address token1, address pool, bool token0Price, uint256 _t0Decimals, uint256 _t1Decimals) internal view returns (uint256) { (uint256 sqrtPriceX96,,,,,,) = IUniswapV3Pool(pool).slot0(); - uint256 rate; if (token0Price) { - rate = (((10 ** IERC20Metadata(token0).decimals() * sqrtPriceX96 >> 96) * sqrtPriceX96) >> 96) * 1e18 / 10 ** IERC20Metadata(token1).decimals(); + return (((_t0Decimals * sqrtPriceX96 >> 96) * sqrtPriceX96) >> 96) * 1e18 / _t1Decimals; } else { - rate = ((10 ** IERC20Metadata(token1).decimals() << 192) / sqrtPriceX96 / sqrtPriceX96) * 1e18 / 10 ** IERC20Metadata(token0).decimals(); + return ((_t1Decimals << 192) / sqrtPriceX96 / sqrtPriceX96) * 1e18 / _t0Decimals; } - return rate; } /// @dev check if token0 is more expensive than token1 given slot0 price & if token0 pricing required @@ -329,10 +334,8 @@ contract OnChainPricingMainnet { } /// @dev check if token0 reserve is bigger than token1 reserve - function _compareUniV3TokenReserves(address token0, address token1, address pool) internal view returns (bool) { - uint256 token0Num = IERC20(token0).balanceOf(pool) / (10 ** IERC20Metadata(token0).decimals()); - uint256 token1Num = IERC20(token1).balanceOf(pool) / (10 ** IERC20Metadata(token1).decimals()); - return token0Num > token1Num; + function _compareUniV3TokenReserves(uint256 _t0Balance, uint256 _t1Balance, uint256 _t0Decimals, uint256 _t1Decimals) internal view returns (bool) { + return (_t0Balance / _t0Decimals) > (_t1Balance / _t1Decimals); } /// @dev query with the address of the token0 & token1 & the fee tier @@ -366,6 +369,48 @@ contract OnChainPricingMainnet { return assetDeltas.length > 0 ? uint256(0 - assetDeltas[assetDeltas.length - 1]) : 0; } + /// @dev Given the input/output token, returns the quote for input amount from Balancer V2 using its underlying math + /// @dev reference: https://hackmd.io/@shuklaayush/BkAtKbCY9 + function getBalancerPriceAnalytically(address tokenIn, uint256 amountIn, address tokenOut) public view returns (uint256) { + bytes32 poolId = getBalancerV2Pool(tokenIn, tokenOut); + if (poolId == BALANCERV2_NONEXIST_POOLID){ + return 0; + } + + (address _pool,) = IBalancerV2Vault(BALANCERV2_VAULT).getPool(poolId); + + uint256 _inDecimals = 10 ** IERC20Metadata(tokenIn).decimals(); + uint256 _outDecimals = 10 ** IERC20Metadata(tokenOut).decimals(); + uint256 _pIn2Out; + + { + uint256[] memory _weights = IBalancerV2WeightedPool(_pool).getNormalizedWeights(); + (address[] memory tokens, uint256[] memory balances, ) = IBalancerV2Vault(BALANCERV2_VAULT).getPoolTokens(poolId); + require(_weights.length == tokens.length, '!lenBAL'); + + uint256 _inTokenIdx = _findTokenInBalancePool(tokenIn, tokens); + require(_inTokenIdx < tokens.length, '!inBAL'); + uint256 _outTokenIdx = _findTokenInBalancePool(tokenOut, tokens); + require(_outTokenIdx < tokens.length, '!outBAL'); + + /// Balancer math for spot price of tokenIn -> tokenOut: weighted value(number * price) relation should be kept + _pIn2Out = (_weights[_inTokenIdx] * balances[_outTokenIdx] / _outDecimals) / (_weights[_outTokenIdx] * balances[_inTokenIdx] / _inDecimals); + } + + return amountIn * _pIn2Out * _outDecimals / _inDecimals; + } + + function _findTokenInBalancePool(address _token, address[] memory _tokens) internal view returns (uint256){ + uint256 _len = _tokens.length; + for (uint256 i = 0; i < _len; ){ + if (_tokens[i] == _token){ + return i; + } + unchecked{ ++i; } + } + return type(uint256).max; + } + /// @dev Given the input/output/connector token, returns the quote for input amount from Balancer V2 function getBalancerPriceWithConnector(address tokenIn, uint256 amountIn, address tokenOut, address connectorToken) public returns (uint256) { bytes32 firstPoolId = getBalancerV2Pool(tokenIn, connectorToken); @@ -394,6 +439,15 @@ contract OnChainPricingMainnet { return assetDeltas.length > 0 ? uint256(0 - assetDeltas[assetDeltas.length - 1]) : 0; } + /// @dev Given the input/output/connector token, returns the quote for input amount from Balancer V2 using its underlying math + function getBalancerPriceWithConnectorAnalytically(address tokenIn, uint256 amountIn, address tokenOut, address connectorToken) public view returns (uint256) { + uint256 _in2ConnectorAmt = getBalancerPriceAnalytically(tokenIn, amountIn, connectorToken); + if (_in2ConnectorAmt <= 0){ + return 0; + } + return getBalancerPriceAnalytically(connectorToken, _in2ConnectorAmt, tokenOut); + } + /// @return selected BalancerV2 pool given the tokenIn and tokenOut function getBalancerV2Pool(address tokenIn, address tokenOut) public view returns(bytes32){ if ((tokenIn == WETH && tokenOut == CREAM) || (tokenOut == WETH && tokenIn == CREAM)){ diff --git a/interfaces/balancer/IBalancerV2Vault.sol b/interfaces/balancer/IBalancerV2Vault.sol index 5d25510..3c3d8ac 100644 --- a/interfaces/balancer/IBalancerV2Vault.sol +++ b/interfaces/balancer/IBalancerV2Vault.sol @@ -28,8 +28,12 @@ struct FundManagement { bool toInternalBalance; } +enum PoolSpecialization { GENERAL, MINIMAL_SWAP_INFO, TWO_TOKEN } + interface IBalancerV2Vault { function batchSwap(SwapKind kind, BatchSwapStep[] calldata swaps, address[] calldata assets, FundManagement calldata funds, int256[] calldata limits, uint256 deadline) external returns (int256[] memory assetDeltas); function queryBatchSwap(SwapKind kind, BatchSwapStep[] calldata swaps, address[] calldata assets, FundManagement calldata funds) external returns (int256[] memory assetDeltas); function swap(SingleSwap calldata singleSwap, FundManagement calldata funds, uint256 limit, uint256 deadline) external returns (uint256 amountCalculatedInOut); + function getPool(bytes32 poolId) external view returns (address, PoolSpecialization); + function getPoolTokens(bytes32 poolId) external view returns (address[] memory tokens, uint256[] memory balances, uint256 lastChangeBlock); } diff --git a/interfaces/balancer/IBalancerV2WeightedPool.sol b/interfaces/balancer/IBalancerV2WeightedPool.sol new file mode 100644 index 0000000..24b9a75 --- /dev/null +++ b/interfaces/balancer/IBalancerV2WeightedPool.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.7.5; +pragma abicoder v2; + +interface IBalancerV2WeightedPool { + function getNormalizedWeights() external view returns (uint256[] memory); +} diff --git a/tests/aura_processor/test_emit_aura.py b/tests/aura_processor/test_emit_aura.py index c4ea425..79d0731 100644 --- a/tests/aura_processor/test_emit_aura.py +++ b/tests/aura_processor/test_emit_aura.py @@ -1,6 +1,5 @@ import brownie from brownie import * -from scripts.send_order import get_cowswap_order """ swapAuraToBveAuraAndEmit diff --git a/tests/aura_processor/test_ragequit_aura.py b/tests/aura_processor/test_ragequit_aura.py index 0d41537..9e66179 100644 --- a/tests/aura_processor/test_ragequit_aura.py +++ b/tests/aura_processor/test_ragequit_aura.py @@ -1,6 +1,5 @@ import brownie from brownie import * -from scripts.send_order import get_cowswap_order """ Unit tests for all functions diff --git a/tests/bribes_processor/test_emit.py b/tests/bribes_processor/test_emit.py index 41455fd..bca3bd9 100644 --- a/tests/bribes_processor/test_emit.py +++ b/tests/bribes_processor/test_emit.py @@ -1,7 +1,5 @@ import brownie from brownie import * -from scripts.send_order import get_cowswap_order - """ swapCVXTobveCVXAndEmit diff --git a/tests/bribes_processor/test_ragequit.py b/tests/bribes_processor/test_ragequit.py index d5c1123..b3508db 100644 --- a/tests/bribes_processor/test_ragequit.py +++ b/tests/bribes_processor/test_ragequit.py @@ -1,6 +1,5 @@ import brownie from brownie import * -from scripts.send_order import get_cowswap_order """ Unit tests for all functions diff --git a/tests/conftest.py b/tests/conftest.py index c0fa47f..a843ba3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,6 +14,7 @@ CVX = "0x4e3fbd56cd56c3e72c1403e103b45db9da5b9d2b" DAI = "0x6b175474e89094c44da98b954eedeac495271d0f" WBTC = "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" +OHM="0x64aa3364F17a4D01c6f1751Fd97C2BD3D7e7f1D5" USDC_WHALE = "0x0a59649758aa4d66e25f08dd01271e891fe52199" BADGER_WHALE = "0xd0a7a8b98957b9cd3cfb9c0425abe44551158e9e" CVX_WHALE = "0xcf50b810e57ac33b91dcf525c6ddd9881b139332" @@ -59,6 +60,10 @@ def processor(lenient_contract): def oneE18(): return 1000000000000000000 +@pytest.fixture +def ohm(): + return interface.ERC20(OHM) + @pytest.fixture def wbtc(): return interface.ERC20(WBTC) diff --git a/tests/on_chain_pricer/test_balancer_pricer.py b/tests/on_chain_pricer/test_balancer_pricer.py index 01d0828..cf652db 100644 --- a/tests/on_chain_pricer/test_balancer_pricer.py +++ b/tests/on_chain_pricer/test_balancer_pricer.py @@ -1,8 +1,8 @@ import brownie from brownie import * -import sys -from scripts.get_price import get_coingecko_price, get_coinmarketcap_price, get_coinmarketcap_metadata +#import sys +#from scripts.get_price import get_coingecko_price, get_coinmarketcap_price, get_coinmarketcap_metadata import pytest @@ -19,9 +19,9 @@ def test_get_balancer_price(oneE18, weth, usdc, pricer): assert quote >= p ## price sanity check with fine liquidity - p1 = get_coingecko_price('ethereum') - p2 = get_coingecko_price('usd-coin') - assert (quote / 1000000) >= (p1 / p2) * 0.98 + #p1 = get_coingecko_price('ethereum') + #p2 = get_coingecko_price('usd-coin') + #assert (quote / 1000000) >= (p1 / p2) * 0.98 """ getBalancerPriceWithConnector quote for token A swapped to token B with connector token C: A -> C -> B @@ -37,10 +37,10 @@ def test_get_balancer_price_with_connector(oneE18, wbtc, usdc, weth, pricer): assert quote >= p ## price sanity check with dime liquidity - yourCMCKey = 'b527d143-8597-474e-b9b2-5c28c1321c37' - p1 = get_coinmarketcap_price('3717', yourCMCKey) ## wbtc - p2 = get_coinmarketcap_price('3408', yourCMCKey) ## usdc - assert (quote / 1000000 / sell_count) >= (p1 / p2) * 0.75 + #yourCMCKey = 'b527d143-8597-474e-b9b2-5c28c1321c37' + #p1 = get_coinmarketcap_price('3717', yourCMCKey) ## wbtc + #p2 = get_coinmarketcap_price('3408', yourCMCKey) ## usdc + #assert (quote / 1000000 / sell_count) >= (p1 / p2) * 0.75 """ getBalancerPrice quote for token A swapped to token B directly using given balancer pool: A - > B @@ -51,4 +51,43 @@ def test_get_balancer_price2(oneE18, cvx, weth, pricer): ## no proper pool in Balancer for WETH in CVX quote = pricer.getBalancerPrice(weth.address, sell_amount, cvx.address).return_value - assert quote == 0 \ No newline at end of file + assert quote == 0 + +""" + getBalancerPriceAnalytically quote for token A swapped to token B directly using given balancer pool: A - > B analytically +""" +def test_get_balancer_price_analytical(oneE18, weth, usdc, pricer): + ## 1e18 + sell_amount = 1 * oneE18 + + ## minimum quote for ETH in USDC(1e6) + p = 1 * 500 * 1000000 + quote = pricer.getBalancerPriceAnalytically(weth.address, sell_amount, usdc.address) + assert quote >= p + +""" + getBalancerPriceWithConnectorAnalytically quote for token A swapped to token B directly using given balancer pool: A - > B analytically +""" +def test_get_balancer_price_with_connector_analytical(oneE18, wbtc, usdc, weth, pricer): + ## 1e8 + sell_count = 10 + sell_amount = sell_count * 100000000 + + ## minimum quote for WBTC in USDC(1e6) + p = sell_count * 10000 * 1000000 + quote = pricer.getBalancerPriceWithConnectorAnalytically(wbtc.address, sell_amount, usdc.address, weth.address) + assert quote >= p + +""" + getBalancerPriceAnalytically quote for token A swapped to token B directly using given balancer pool: A - > B analytically +""" +def test_get_balancer_price_ohm__analytical(oneE18, ohm, dai, pricer): + ## 1e8 + sell_count = 1000 + sell_amount = sell_count * oneE18 + + ## minimum quote for OHM in DAI(1e6) + p = sell_count * 10 * oneE18 + quote = pricer.getBalancerPriceAnalytically(ohm.address, sell_amount, dai.address) + assert quote >= p + \ No newline at end of file diff --git a/tests/on_chain_pricer/test_univ3_pricer.py b/tests/on_chain_pricer/test_univ3_pricer.py index b1f28e1..d29aa21 100644 --- a/tests/on_chain_pricer/test_univ3_pricer.py +++ b/tests/on_chain_pricer/test_univ3_pricer.py @@ -22,11 +22,12 @@ def test_get_univ3_price_with_connector(oneE18, wbtc, usdc, weth, pricer): ## 1e8 sell_amount = 100 * 100000000 - quote = pricer.getUniV3Price(wbtc.address, sell_amount, usdc.address).return_value + ## minimum quote for WBTC in USDC(1e6) ## Rip ETH price + p = 100 * 15000 * 1000000 quoteWithConnector = pricer.getUniV3PriceWithConnector(wbtc.address, sell_amount, usdc.address, weth.address).return_value ## min price - assert quoteWithConnector > quote + assert quoteWithConnector >= p """ getUniV3PriceWithConnector quote for stablecoin A swapped to stablecoin B with connector token C: A -> C -> B @@ -35,8 +36,9 @@ def test_get_univ3_price_with_connector_stablecoin(oneE18, dai, usdc, weth, pric ## 1e18 sell_amount = 10000 * oneE18 - quote = pricer.getUniV3Price(dai.address, sell_amount, usdc.address).return_value + ## minimum quote for DAI in USDC(1e6) ## Rip ETH price + p = 10000 * 0.99 * 1000000 quoteWithConnector = pricer.getUniV3PriceWithConnector(dai.address, sell_amount, usdc.address, weth.address).return_value ## min price - assert quoteWithConnector < quote \ No newline at end of file + assert quoteWithConnector >= p \ No newline at end of file From 32cdabd40a131f31aeeb4a6c0f0621706fb4c1b9 Mon Sep 17 00:00:00 2001 From: rayeaster Date: Fri, 15 Jul 2022 18:50:36 +0800 Subject: [PATCH 02/53] add Uniswap V3 simulation --- brownie-config.yml | 2 +- contracts/OnChainPricingMainnet.sol | 58 ++++- contracts/OnChainPricingMainnetLenient.sol | 3 + contracts/UniV3SwapSimulator.sol | 103 ++++++++ contracts/libraries/uniswap/BitMath.sol | 94 ++++++++ contracts/libraries/uniswap/FixedPoint96.sol | 9 + contracts/libraries/uniswap/FullMath.sol | 123 ++++++++++ contracts/libraries/uniswap/LiquidityMath.sol | 18 ++ .../libraries/uniswap/LowGasSafeMath.sol | 46 ++++ contracts/libraries/uniswap/SafeCast.sol | 28 +++ contracts/libraries/uniswap/SqrtPriceMath.sol | 226 ++++++++++++++++++ contracts/libraries/uniswap/SwapMath.sol | 102 ++++++++ contracts/libraries/uniswap/TickBitmap.sol | 68 ++++++ contracts/libraries/uniswap/TickMath.sol | 204 ++++++++++++++++ contracts/libraries/uniswap/UnsafeMath.sol | 17 ++ interfaces/uniswap/IV3Pool.sol | 8 +- interfaces/uniswap/IV3Simulator.sol | 7 + tests/conftest.py | 7 +- .../on_chain_pricer/test_univ3_pricer_simu.py | 31 +++ 19 files changed, 1145 insertions(+), 9 deletions(-) create mode 100644 contracts/UniV3SwapSimulator.sol create mode 100644 contracts/libraries/uniswap/BitMath.sol create mode 100644 contracts/libraries/uniswap/FixedPoint96.sol create mode 100644 contracts/libraries/uniswap/FullMath.sol create mode 100644 contracts/libraries/uniswap/LiquidityMath.sol create mode 100644 contracts/libraries/uniswap/LowGasSafeMath.sol create mode 100644 contracts/libraries/uniswap/SafeCast.sol create mode 100644 contracts/libraries/uniswap/SqrtPriceMath.sol create mode 100644 contracts/libraries/uniswap/SwapMath.sol create mode 100644 contracts/libraries/uniswap/TickBitmap.sol create mode 100644 contracts/libraries/uniswap/TickMath.sol create mode 100644 contracts/libraries/uniswap/UnsafeMath.sol create mode 100644 interfaces/uniswap/IV3Simulator.sol create mode 100644 tests/on_chain_pricer/test_univ3_pricer_simu.py diff --git a/brownie-config.yml b/brownie-config.yml index 4b6462b..f05a560 100644 --- a/brownie-config.yml +++ b/brownie-config.yml @@ -13,7 +13,7 @@ dependencies: # path remapping to support imports from GitHub/NPM compiler: solc: - version: 0.8.10 + # version: 0.8.10 remappings: - "@oz=OpenZeppelin/openzeppelin-contracts@4.5.0/contracts/" diff --git a/contracts/OnChainPricingMainnet.sol b/contracts/OnChainPricingMainnet.sol index c5d3db3..2e5ebe2 100644 --- a/contracts/OnChainPricingMainnet.sol +++ b/contracts/OnChainPricingMainnet.sol @@ -15,6 +15,7 @@ import "../interfaces/balancer/IBalancerV2Vault.sol"; import "../interfaces/balancer/IBalancerV2WeightedPool.sol"; import "../interfaces/curve/ICurveRouter.sol"; import "../interfaces/curve/ICurvePool.sol"; +import "../interfaces/uniswap/IV3Simulator.sol"; enum SwapType { CURVE, //0 @@ -106,6 +107,27 @@ contract OnChainPricingMainnet { address public constant AURABAL = 0x616e8BfA43F920657B3497DBf40D6b1A02D4608d; address public constant BALWETHBPT = 0x5c6Ee304399DBdB9C8Ef030aB642B10820DB8F56; uint256 public constant CURVE_FEE_SCALE = 100000; + + /// @dev helper library to simulate Uniswap V3 swap + address public uniV3Simulator; + /// @dev slippage allowance bps in Uniswap V3 simulation, max 10000 + uint256 public uniV3SimSlippageBps = 100; // 1% slippage allowed in simulation + + /// === TEST-ONLY === + constructor(address _uniV3Simulator){ + uniV3Simulator = _uniV3Simulator; + } + + function setUniV3Simulator(address _uniV3Simulator) external { + require(_uniV3Simulator != address(0));//TODO permission + uniV3Simulator = _uniV3Simulator; + } + + function setUniV3SimuSlippageBps(uint256 _slippageBps) external { + require(_slippageBps < 10000);//TODO permission + uniV3SimSlippageBps = _slippageBps; + } + /// === END TEST-ONLY === struct Quote { SwapType name; @@ -230,6 +252,32 @@ contract OnChainPricingMainnet { /// === UNIV3 === /// + /// @dev simulate Uniswap V3 swap using its tick-based math for given parameters + /// @dev check library UniV3SwapTick for more + function simulateUniV3Swap(address _simulator, address tokenIn, uint256 amountIn, address tokenOut, uint24 _fee, uint256 _slippageAllowedBps) public view returns (uint256){ + (address token0, address token1, bool token0Price) = _ifUniV3Token0Price(tokenIn, tokenOut); + address _pool = _getUniV3PoolAddress(token0, token1, _fee); + + { + // heuristic check0: ensure the pool [exist] and properly initiated with valid in-range liquidity + if (!_pool.isContract() || IUniswapV3Pool(_pool).liquidity() == 0) { + return 0; + } + } + + { + uint256 _t0Balance = IERC20(token0).balanceOf(_pool); + uint256 _t1Balance = IERC20(token1).balanceOf(_pool); + + // heuristic check1: ensure the pool tokenIn reserve makes sense in terms of [amountIn] + if ((token0Price? _t0Balance : _t1Balance) <= amountIn){ + return 0; + } + } + + return IUniswapV3Simulator(_simulator).simulateUniV3Swap(_pool, token0, token1, token0Price, _fee, amountIn, _slippageAllowedBps); + } + /// @dev Given the address of the input token & amount & the output token /// @return the quote for it function getUniV3Price(address tokenIn, uint256 amountIn, address tokenOut) public returns (uint256) { @@ -239,11 +287,11 @@ contract OnChainPricingMainnet { uint256 feeTypes = univ3_fees.length; for (uint256 i = 0; i < feeTypes; ){ //filter out disqualified pools to save gas on quoter swap query - uint256 rate = _getUniV3Rate(token0, token1, univ3_fees[i], token0Price, amountIn); - if (rate > 0){ - //uint256 quote = _getUniV3QuoterQuery(tokenIn, tokenOut, univ3_fees[i], amountIn);// skip this "call and revert" gas-consuming method - if (rate > quoteRate){ - quoteRate = rate; + { + uint256 rate = simulateUniV3Swap(uniV3Simulator, tokenIn, amountIn, tokenOut, univ3_fees[i], uniV3SimSlippageBps);//_getUniV3Rate(token0, token1, univ3_fees[i], token0Price, amountIn) + if (rate > 0){ + //uint256 quote = _getUniV3QuoterQuery(tokenIn, tokenOut, univ3_fees[i], amountIn);// skip this "call and revert" gas-consuming method + quoteRate = rate > quoteRate? rate : quoteRate; } } diff --git a/contracts/OnChainPricingMainnetLenient.sol b/contracts/OnChainPricingMainnetLenient.sol index 8b4a784..ed1cbbe 100644 --- a/contracts/OnChainPricingMainnetLenient.sol +++ b/contracts/OnChainPricingMainnetLenient.sol @@ -32,6 +32,9 @@ contract OnChainPricingMainnetLenient is OnChainPricingMainnet { uint256 public slippage = 200; // 2% Initially + constructor(address _uniV3Simulator) OnChainPricingMainnet(_uniV3Simulator){ + + } function setSlippage(uint256 newSlippage) external { require(msg.sender == TECH_OPS, "Only TechOps"); diff --git a/contracts/UniV3SwapSimulator.sol b/contracts/UniV3SwapSimulator.sol new file mode 100644 index 0000000..bc1843a --- /dev/null +++ b/contracts/UniV3SwapSimulator.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.7.6; // some underlying uniswap library require version <0.8.0 +pragma abicoder v2; + +import "./libraries/uniswap/SwapMath.sol"; +import "./libraries/uniswap/TickBitmap.sol"; +import "./libraries/uniswap/TickMath.sol"; +import "./libraries/uniswap/LiquidityMath.sol"; + +interface IUniswapV3PoolSwapTick { + function slot0() external view returns (uint160 sqrtPriceX96, int24, uint16, uint16, uint16, uint8, bool); + function liquidity() external view returns (uint128); + function tickSpacing() external view returns (int24); + function ticks(int24 tick) external view returns (uint128 liquidityGross, int128 liquidityNet, uint256 feeGrowthOutside0X128, uint256 feeGrowthOutside1X128, int56 tickCumulativeOutside, uint160 secondsPerLiquidityOutsideX128, uint32 secondsOutside, bool initialized); +} + +// simplified version of https://github.com/Uniswap/v3-core/blob/main/contracts/UniswapV3Pool.sol#L561 +struct SwapStatus{ + int256 _amountSpecifiedRemaining; + uint160 _sqrtPriceX96; + int24 _tick; + uint128 _liquidity; + int256 _amountCalculated; +} + +/// @dev Swap Simulator for Uniswap V3 +contract UniV3SwapSimulator { + using LowGasSafeMath for uint256; + using LowGasSafeMath for int256; + using SafeCast for uint256; + using SafeCast for int256; + + /// @dev View function which aims to simplify Uniswap V3 swap logic (no oracle/fee update, etc) to + /// @dev estimate the expected output for given swap parameters and slippage + /// @dev simplified version of https://github.com/Uniswap/v3-core/blob/main/contracts/UniswapV3Pool.sol#L596 + /// @return simulated output token amount using Uniswap V3 tick-based math + function simulateUniV3Swap(address _pool, address _token0, address _token1, bool _zeroForOne, uint24 _fee, uint256 _amountIn, uint256 _slippageAllowedBps) external view returns (uint256){ + require(_slippageAllowedBps < 10000, '!SLIP'); + require(_slippageAllowedBps > 0, '!SLI0'); + + // Get current state of the pool + int24 _tickSpacing = IUniswapV3PoolSwapTick(_pool).tickSpacing(); + // lower limit if zeroForOne in terms of slippage, or upper limit for the other direction + uint160 _sqrtPriceLimitX96; + // Temporary state holding key data across swap steps + SwapStatus memory state; + + { + (uint160 _currentPX96, int24 _currentTick,,,,,) = IUniswapV3PoolSwapTick(_pool).slot0(); + _sqrtPriceLimitX96 = _zeroForOne? (_currentPX96 * (10000 - _slippageAllowedBps).toUint160() / 10000) : (_currentPX96 * (10000 + _slippageAllowedBps).toUint160() / 10000); + state = SwapStatus(_amountIn.toInt256(), _currentPX96, _currentTick, IUniswapV3PoolSwapTick(_pool).liquidity(), 0); + } + + // Loop over ticks until we exhaust all _amountIn or hit the slippage-allowed price limit + while (state._amountSpecifiedRemaining != 0 && state._sqrtPriceX96 != _sqrtPriceLimitX96) { + { + _stepInTick(state, TickNextWithWordQuery(_pool, state._tick, _tickSpacing, _zeroForOne), _fee, _zeroForOne, _sqrtPriceLimitX96); + } + } + + return uint256(state._amountCalculated); + } + + /// @dev swap step in the tick + function _stepInTick(SwapStatus memory state, TickNextWithWordQuery memory _nextTickQuery, uint24 _fee, bool _zeroForOne, uint160 _sqrtPriceLimitX96) view internal{ + + /// Fetch NEXT-STEP tick to prepare for crossing + (int24 tickNext, bool initialized) = TickBitmap.nextInitializedTickWithinOneWord(_nextTickQuery); + uint160 sqrtPriceNextX96 = TickMath.getSqrtRatioAtTick(tickNext); + uint160 sqrtPriceStartX96 = state._sqrtPriceX96; + uint160 _targetPX96 = (_zeroForOne ? sqrtPriceNextX96 < _sqrtPriceLimitX96 : sqrtPriceNextX96 > _sqrtPriceLimitX96)? _sqrtPriceLimitX96 : sqrtPriceNextX96; + + /// Trying to perform in-tick swap + { + _swapCalculation(state, _targetPX96, _fee); + } + + /// Check if we have to cross ticks for NEXT-STEP + if (state._sqrtPriceX96 == sqrtPriceNextX96) { + // if the tick is initialized, run the tick transition + if (initialized) { + (,int128 liquidityNet,,,,,,) = IUniswapV3PoolSwapTick(_nextTickQuery.pool).ticks(tickNext); + // if we're moving leftward, we interpret liquidityNet as the opposite sign safe because liquidityNet cannot be type(int128).min + if (_zeroForOne) liquidityNet = -liquidityNet; + state._liquidity = LiquidityMath.addDelta(state._liquidity, liquidityNet); + } + state._tick = _zeroForOne ? tickNext - 1 : tickNext; + } else if (state._sqrtPriceX96 != sqrtPriceStartX96) { + // recompute unless we're on a lower tick boundary (i.e. already transitioned ticks), and haven't moved + state._tick = TickMath.getTickAtSqrtRatio(state._sqrtPriceX96); + } + } + + function _swapCalculation(SwapStatus memory state, uint160 _targetPX96, uint24 _fee) internal view { + (uint160 sqrtPriceX96, uint256 amountIn, uint256 amountOut, uint256 feeAmount) = SwapMath.computeSwapStep(state._sqrtPriceX96, _targetPX96, state._liquidity, state._amountSpecifiedRemaining, _fee); + + /// Update amounts for swap pair tokens + state._sqrtPriceX96 = sqrtPriceX96; + state._amountSpecifiedRemaining -= (amountIn + feeAmount).toInt256(); + state._amountCalculated = state._amountCalculated.add(amountOut.toInt256()); + } + +} \ No newline at end of file diff --git a/contracts/libraries/uniswap/BitMath.sol b/contracts/libraries/uniswap/BitMath.sol new file mode 100644 index 0000000..2b984d2 --- /dev/null +++ b/contracts/libraries/uniswap/BitMath.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.7.6; +pragma abicoder v2; + +// https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/BitMath.sol +library BitMath { + /// @notice Returns the index of the most significant bit of the number, + /// where the least significant bit is at index 0 and the most significant bit is at index 255 + /// @dev The function satisfies the property: + /// x >= 2**mostSignificantBit(x) and x < 2**(mostSignificantBit(x)+1) + /// @param x the value for which to compute the most significant bit, must be greater than 0 + /// @return r the index of the most significant bit + function mostSignificantBit(uint256 x) internal pure returns (uint8 r) { + require(x > 0); + + if (x >= 0x100000000000000000000000000000000) { + x >>= 128; + r += 128; + } + if (x >= 0x10000000000000000) { + x >>= 64; + r += 64; + } + if (x >= 0x100000000) { + x >>= 32; + r += 32; + } + if (x >= 0x10000) { + x >>= 16; + r += 16; + } + if (x >= 0x100) { + x >>= 8; + r += 8; + } + if (x >= 0x10) { + x >>= 4; + r += 4; + } + if (x >= 0x4) { + x >>= 2; + r += 2; + } + if (x >= 0x2) r += 1; + } + + /// @notice Returns the index of the least significant bit of the number, + /// where the least significant bit is at index 0 and the most significant bit is at index 255 + /// @dev The function satisfies the property: + /// (x & 2**leastSignificantBit(x)) != 0 and (x & (2**(leastSignificantBit(x)) - 1)) == 0) + /// @param x the value for which to compute the least significant bit, must be greater than 0 + /// @return r the index of the least significant bit + function leastSignificantBit(uint256 x) internal pure returns (uint8 r) { + require(x > 0); + + r = 255; + if (x & type(uint128).max > 0) { + r -= 128; + } else { + x >>= 128; + } + if (x & type(uint64).max > 0) { + r -= 64; + } else { + x >>= 64; + } + if (x & type(uint32).max > 0) { + r -= 32; + } else { + x >>= 32; + } + if (x & type(uint16).max > 0) { + r -= 16; + } else { + x >>= 16; + } + if (x & type(uint8).max > 0) { + r -= 8; + } else { + x >>= 8; + } + if (x & 0xf > 0) { + r -= 4; + } else { + x >>= 4; + } + if (x & 0x3 > 0) { + r -= 2; + } else { + x >>= 2; + } + if (x & 0x1 > 0) r -= 1; + } +} \ No newline at end of file diff --git a/contracts/libraries/uniswap/FixedPoint96.sol b/contracts/libraries/uniswap/FixedPoint96.sol new file mode 100644 index 0000000..496094e --- /dev/null +++ b/contracts/libraries/uniswap/FixedPoint96.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.7.6; +pragma abicoder v2; + +// https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/FixedPoint96.sol +library FixedPoint96 { + uint8 internal constant RESOLUTION = 96; + uint256 internal constant Q96 = 0x1000000000000000000000000; +} \ No newline at end of file diff --git a/contracts/libraries/uniswap/FullMath.sol b/contracts/libraries/uniswap/FullMath.sol new file mode 100644 index 0000000..19ff5a7 --- /dev/null +++ b/contracts/libraries/uniswap/FullMath.sol @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.7.6; +pragma abicoder v2; + +// https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/FullMath.sol +library FullMath { + /// @notice Calculates floor(a×b÷denominator) with full precision. Throws if result overflows a uint256 or denominator == 0 + /// @param a The multiplicand + /// @param b The multiplier + /// @param denominator The divisor + /// @return result The 256-bit result + /// @dev Credit to Remco Bloemen under MIT license https://xn--2-umb.com/21/muldiv + function mulDiv( + uint256 a, + uint256 b, + uint256 denominator + ) internal pure returns (uint256 result) { + // 512-bit multiply [prod1 prod0] = a * b + // Compute the product mod 2**256 and mod 2**256 - 1 + // then use the Chinese Remainder Theorem to reconstruct + // the 512 bit result. The result is stored in two 256 + // variables such that product = prod1 * 2**256 + prod0 + uint256 prod0; // Least significant 256 bits of the product + uint256 prod1; // Most significant 256 bits of the product + assembly { + let mm := mulmod(a, b, not(0)) + prod0 := mul(a, b) + prod1 := sub(sub(mm, prod0), lt(mm, prod0)) + } + + // Handle non-overflow cases, 256 by 256 division + if (prod1 == 0) { + require(denominator > 0); + assembly { + result := div(prod0, denominator) + } + return result; + } + + // Make sure the result is less than 2**256. + // Also prevents denominator == 0 + require(denominator > prod1); + + /////////////////////////////////////////////// + // 512 by 256 division. + /////////////////////////////////////////////// + + // Make division exact by subtracting the remainder from [prod1 prod0] + // Compute remainder using mulmod + uint256 remainder; + assembly { + remainder := mulmod(a, b, denominator) + } + // Subtract 256 bit number from 512 bit number + assembly { + prod1 := sub(prod1, gt(remainder, prod0)) + prod0 := sub(prod0, remainder) + } + + // Factor powers of two out of denominator + // Compute largest power of two divisor of denominator. + // Always >= 1. + uint256 twos = -denominator & denominator; + // Divide denominator by power of two + assembly { + denominator := div(denominator, twos) + } + + // Divide [prod1 prod0] by the factors of two + assembly { + prod0 := div(prod0, twos) + } + // Shift in bits from prod1 into prod0. For this we need + // to flip `twos` such that it is 2**256 / twos. + // If twos is zero, then it becomes one + assembly { + twos := add(div(sub(0, twos), twos), 1) + } + prod0 |= prod1 * twos; + + // Invert denominator mod 2**256 + // Now that denominator is an odd number, it has an inverse + // modulo 2**256 such that denominator * inv = 1 mod 2**256. + // Compute the inverse by starting with a seed that is correct + // correct for four bits. That is, denominator * inv = 1 mod 2**4 + uint256 inv = (3 * denominator) ^ 2; + // Now use Newton-Raphson iteration to improve the precision. + // Thanks to Hensel's lifting lemma, this also works in modular + // arithmetic, doubling the correct bits in each step. + inv *= 2 - denominator * inv; // inverse mod 2**8 + inv *= 2 - denominator * inv; // inverse mod 2**16 + inv *= 2 - denominator * inv; // inverse mod 2**32 + inv *= 2 - denominator * inv; // inverse mod 2**64 + inv *= 2 - denominator * inv; // inverse mod 2**128 + inv *= 2 - denominator * inv; // inverse mod 2**256 + + // Because the division is now exact we can divide by multiplying + // with the modular inverse of denominator. This will give us the + // correct result modulo 2**256. Since the precoditions guarantee + // that the outcome is less than 2**256, this is the final result. + // We don't need to compute the high bits of the result and prod1 + // is no longer required. + result = prod0 * inv; + return result; + } + + /// @notice Calculates ceil(a×b÷denominator) with full precision. Throws if result overflows a uint256 or denominator == 0 + /// @param a The multiplicand + /// @param b The multiplier + /// @param denominator The divisor + /// @return result The 256-bit result + function mulDivRoundingUp( + uint256 a, + uint256 b, + uint256 denominator + ) internal pure returns (uint256 result) { + result = mulDiv(a, b, denominator); + if (mulmod(a, b, denominator) > 0) { + require(result < type(uint256).max); + result++; + } + } +} \ No newline at end of file diff --git a/contracts/libraries/uniswap/LiquidityMath.sol b/contracts/libraries/uniswap/LiquidityMath.sol new file mode 100644 index 0000000..260df90 --- /dev/null +++ b/contracts/libraries/uniswap/LiquidityMath.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.7.6; +pragma abicoder v2; + +// https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/LiquidityMath.sol +library LiquidityMath { + /// @notice Add a signed liquidity delta to liquidity and revert if it overflows or underflows + /// @param x The liquidity before change + /// @param y The delta by which liquidity should be changed + /// @return z The liquidity delta + function addDelta(uint128 x, int128 y) internal pure returns (uint128 z) { + if (y < 0) { + require((z = x - uint128(-y)) < x, 'LS'); + } else { + require((z = x + uint128(y)) >= x, 'LA'); + } + } +} \ No newline at end of file diff --git a/contracts/libraries/uniswap/LowGasSafeMath.sol b/contracts/libraries/uniswap/LowGasSafeMath.sol new file mode 100644 index 0000000..fd5dc12 --- /dev/null +++ b/contracts/libraries/uniswap/LowGasSafeMath.sol @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.7.6; +pragma abicoder v2; + +// https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/LowGasSafeMath.sol +library LowGasSafeMath { + /// @notice Returns x + y, reverts if sum overflows uint256 + /// @param x The augend + /// @param y The addend + /// @return z The sum of x and y + function add(uint256 x, uint256 y) internal pure returns (uint256 z) { + require((z = x + y) >= x); + } + + /// @notice Returns x - y, reverts if underflows + /// @param x The minuend + /// @param y The subtrahend + /// @return z The difference of x and y + function sub(uint256 x, uint256 y) internal pure returns (uint256 z) { + require((z = x - y) <= x); + } + + /// @notice Returns x * y, reverts if overflows + /// @param x The multiplicand + /// @param y The multiplier + /// @return z The product of x and y + function mul(uint256 x, uint256 y) internal pure returns (uint256 z) { + require(x == 0 || (z = x * y) / x == y); + } + + /// @notice Returns x + y, reverts if overflows or underflows + /// @param x The augend + /// @param y The addend + /// @return z The sum of x and y + function add(int256 x, int256 y) internal pure returns (int256 z) { + require((z = x + y) >= x == (y >= 0)); + } + + /// @notice Returns x - y, reverts if overflows or underflows + /// @param x The minuend + /// @param y The subtrahend + /// @return z The difference of x and y + function sub(int256 x, int256 y) internal pure returns (int256 z) { + require((z = x - y) <= x == (y >= 0)); + } +} \ No newline at end of file diff --git a/contracts/libraries/uniswap/SafeCast.sol b/contracts/libraries/uniswap/SafeCast.sol new file mode 100644 index 0000000..648022b --- /dev/null +++ b/contracts/libraries/uniswap/SafeCast.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.7.6; +pragma abicoder v2; + +// https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/SafeCast.sol +library SafeCast { + /// @notice Cast a uint256 to a uint160, revert on overflow + /// @param y The uint256 to be downcasted + /// @return z The downcasted integer, now type uint160 + function toUint160(uint256 y) internal pure returns (uint160 z) { + require((z = uint160(y)) == y); + } + + /// @notice Cast a int256 to a int128, revert on overflow or underflow + /// @param y The int256 to be downcasted + /// @return z The downcasted integer, now type int128 + function toInt128(int256 y) internal pure returns (int128 z) { + require((z = int128(y)) == y); + } + + /// @notice Cast a uint256 to a int256, revert on overflow + /// @param y The uint256 to be casted + /// @return z The casted integer, now type int256 + function toInt256(uint256 y) internal pure returns (int256 z) { + require(y < 2**255); + z = int256(y); + } +} \ No newline at end of file diff --git a/contracts/libraries/uniswap/SqrtPriceMath.sol b/contracts/libraries/uniswap/SqrtPriceMath.sol new file mode 100644 index 0000000..18b9678 --- /dev/null +++ b/contracts/libraries/uniswap/SqrtPriceMath.sol @@ -0,0 +1,226 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.7.6; +pragma abicoder v2; + +import "./LowGasSafeMath.sol"; +import "./SafeCast.sol"; +import "./FullMath.sol"; +import "./UnsafeMath.sol"; +import "./FixedPoint96.sol"; + +// https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/SqrtPriceMath.sol +library SqrtPriceMath { + using LowGasSafeMath for uint256; + using SafeCast for uint256; + + /// @notice Gets the next sqrt price given a delta of token0 + /// @dev Always rounds up, because in the exact output case (increasing price) we need to move the price at least + /// far enough to get the desired output amount, and in the exact input case (decreasing price) we need to move the + /// price less in order to not send too much output. + /// The most precise formula for this is liquidity * sqrtPX96 / (liquidity +- amount * sqrtPX96), + /// if this is impossible because of overflow, we calculate liquidity / (liquidity / sqrtPX96 +- amount). + /// @param sqrtPX96 The starting price, i.e. before accounting for the token0 delta + /// @param liquidity The amount of usable liquidity + /// @param amount How much of token0 to add or remove from virtual reserves + /// @param add Whether to add or remove the amount of token0 + /// @return The price after adding or removing amount, depending on add + function getNextSqrtPriceFromAmount0RoundingUp( + uint160 sqrtPX96, + uint128 liquidity, + uint256 amount, + bool add + ) internal pure returns (uint160) { + // we short circuit amount == 0 because the result is otherwise not guaranteed to equal the input price + if (amount == 0) return sqrtPX96; + uint256 numerator1 = uint256(liquidity) << FixedPoint96.RESOLUTION; + + if (add) { + uint256 product; + if ((product = amount * sqrtPX96) / amount == sqrtPX96) { + uint256 denominator = numerator1 + product; + if (denominator >= numerator1) + // always fits in 160 bits + return uint160(FullMath.mulDivRoundingUp(numerator1, sqrtPX96, denominator)); + } + + return uint160(UnsafeMath.divRoundingUp(numerator1, (numerator1 / sqrtPX96).add(amount))); + } else { + uint256 product; + // if the product overflows, we know the denominator underflows + // in addition, we must check that the denominator does not underflow + require((product = amount * sqrtPX96) / amount == sqrtPX96 && numerator1 > product); + uint256 denominator = numerator1 - product; + return FullMath.mulDivRoundingUp(numerator1, sqrtPX96, denominator).toUint160(); + } + } + + /// @notice Gets the next sqrt price given a delta of token1 + /// @dev Always rounds down, because in the exact output case (decreasing price) we need to move the price at least + /// far enough to get the desired output amount, and in the exact input case (increasing price) we need to move the + /// price less in order to not send too much output. + /// The formula we compute is within <1 wei of the lossless version: sqrtPX96 +- amount / liquidity + /// @param sqrtPX96 The starting price, i.e., before accounting for the token1 delta + /// @param liquidity The amount of usable liquidity + /// @param amount How much of token1 to add, or remove, from virtual reserves + /// @param add Whether to add, or remove, the amount of token1 + /// @return The price after adding or removing `amount` + function getNextSqrtPriceFromAmount1RoundingDown( + uint160 sqrtPX96, + uint128 liquidity, + uint256 amount, + bool add + ) internal pure returns (uint160) { + // if we're adding (subtracting), rounding down requires rounding the quotient down (up) + // in both cases, avoid a mulDiv for most inputs + if (add) { + uint256 quotient = + ( + amount <= type(uint160).max + ? (amount << FixedPoint96.RESOLUTION) / liquidity + : FullMath.mulDiv(amount, FixedPoint96.Q96, liquidity) + ); + + return uint256(sqrtPX96).add(quotient).toUint160(); + } else { + uint256 quotient = + ( + amount <= type(uint160).max + ? UnsafeMath.divRoundingUp(amount << FixedPoint96.RESOLUTION, liquidity) + : FullMath.mulDivRoundingUp(amount, FixedPoint96.Q96, liquidity) + ); + + require(sqrtPX96 > quotient); + // always fits 160 bits + return uint160(sqrtPX96 - quotient); + } + } + + /// @notice Gets the next sqrt price given an input amount of token0 or token1 + /// @dev Throws if price or liquidity are 0, or if the next price is out of bounds + /// @param sqrtPX96 The starting price, i.e., before accounting for the input amount + /// @param liquidity The amount of usable liquidity + /// @param amountIn How much of token0, or token1, is being swapped in + /// @param zeroForOne Whether the amount in is token0 or token1 + /// @return sqrtQX96 The price after adding the input amount to token0 or token1 + function getNextSqrtPriceFromInput( + uint160 sqrtPX96, + uint128 liquidity, + uint256 amountIn, + bool zeroForOne + ) internal pure returns (uint160 sqrtQX96) { + require(sqrtPX96 > 0); + require(liquidity > 0); + + // round to make sure that we don't pass the target price + return + zeroForOne + ? getNextSqrtPriceFromAmount0RoundingUp(sqrtPX96, liquidity, amountIn, true) + : getNextSqrtPriceFromAmount1RoundingDown(sqrtPX96, liquidity, amountIn, true); + } + + /// @notice Gets the next sqrt price given an output amount of token0 or token1 + /// @dev Throws if price or liquidity are 0 or the next price is out of bounds + /// @param sqrtPX96 The starting price before accounting for the output amount + /// @param liquidity The amount of usable liquidity + /// @param amountOut How much of token0, or token1, is being swapped out + /// @param zeroForOne Whether the amount out is token0 or token1 + /// @return sqrtQX96 The price after removing the output amount of token0 or token1 + function getNextSqrtPriceFromOutput( + uint160 sqrtPX96, + uint128 liquidity, + uint256 amountOut, + bool zeroForOne + ) internal pure returns (uint160 sqrtQX96) { + require(sqrtPX96 > 0); + require(liquidity > 0); + + // round to make sure that we pass the target price + return + zeroForOne + ? getNextSqrtPriceFromAmount1RoundingDown(sqrtPX96, liquidity, amountOut, false) + : getNextSqrtPriceFromAmount0RoundingUp(sqrtPX96, liquidity, amountOut, false); + } + + /// @notice Gets the amount0 delta between two prices + /// @dev Calculates liquidity / sqrt(lower) - liquidity / sqrt(upper), + /// i.e. liquidity * (sqrt(upper) - sqrt(lower)) / (sqrt(upper) * sqrt(lower)) + /// @param sqrtRatioAX96 A sqrt price + /// @param sqrtRatioBX96 Another sqrt price + /// @param liquidity The amount of usable liquidity + /// @param roundUp Whether to round the amount up or down + /// @return amount0 Amount of token0 required to cover a position of size liquidity between the two passed prices + function getAmount0Delta( + uint160 sqrtRatioAX96, + uint160 sqrtRatioBX96, + uint128 liquidity, + bool roundUp + ) internal pure returns (uint256 amount0) { + if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); + + uint256 numerator1 = uint256(liquidity) << FixedPoint96.RESOLUTION; + uint256 numerator2 = sqrtRatioBX96 - sqrtRatioAX96; + + require(sqrtRatioAX96 > 0); + + return + roundUp + ? UnsafeMath.divRoundingUp( + FullMath.mulDivRoundingUp(numerator1, numerator2, sqrtRatioBX96), + sqrtRatioAX96 + ) + : FullMath.mulDiv(numerator1, numerator2, sqrtRatioBX96) / sqrtRatioAX96; + } + + /// @notice Gets the amount1 delta between two prices + /// @dev Calculates liquidity * (sqrt(upper) - sqrt(lower)) + /// @param sqrtRatioAX96 A sqrt price + /// @param sqrtRatioBX96 Another sqrt price + /// @param liquidity The amount of usable liquidity + /// @param roundUp Whether to round the amount up, or down + /// @return amount1 Amount of token1 required to cover a position of size liquidity between the two passed prices + function getAmount1Delta( + uint160 sqrtRatioAX96, + uint160 sqrtRatioBX96, + uint128 liquidity, + bool roundUp + ) internal pure returns (uint256 amount1) { + if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); + + return + roundUp + ? FullMath.mulDivRoundingUp(liquidity, sqrtRatioBX96 - sqrtRatioAX96, FixedPoint96.Q96) + : FullMath.mulDiv(liquidity, sqrtRatioBX96 - sqrtRatioAX96, FixedPoint96.Q96); + } + + /// @notice Helper that gets signed token0 delta + /// @param sqrtRatioAX96 A sqrt price + /// @param sqrtRatioBX96 Another sqrt price + /// @param liquidity The change in liquidity for which to compute the amount0 delta + /// @return amount0 Amount of token0 corresponding to the passed liquidityDelta between the two prices + function getAmount0Delta( + uint160 sqrtRatioAX96, + uint160 sqrtRatioBX96, + int128 liquidity + ) internal pure returns (int256 amount0) { + return + liquidity < 0 + ? -getAmount0Delta(sqrtRatioAX96, sqrtRatioBX96, uint128(-liquidity), false).toInt256() + : getAmount0Delta(sqrtRatioAX96, sqrtRatioBX96, uint128(liquidity), true).toInt256(); + } + + /// @notice Helper that gets signed token1 delta + /// @param sqrtRatioAX96 A sqrt price + /// @param sqrtRatioBX96 Another sqrt price + /// @param liquidity The change in liquidity for which to compute the amount1 delta + /// @return amount1 Amount of token1 corresponding to the passed liquidityDelta between the two prices + function getAmount1Delta( + uint160 sqrtRatioAX96, + uint160 sqrtRatioBX96, + int128 liquidity + ) internal pure returns (int256 amount1) { + return + liquidity < 0 + ? -getAmount1Delta(sqrtRatioAX96, sqrtRatioBX96, uint128(-liquidity), false).toInt256() + : getAmount1Delta(sqrtRatioAX96, sqrtRatioBX96, uint128(liquidity), true).toInt256(); + } +} \ No newline at end of file diff --git a/contracts/libraries/uniswap/SwapMath.sol b/contracts/libraries/uniswap/SwapMath.sol new file mode 100644 index 0000000..033366a --- /dev/null +++ b/contracts/libraries/uniswap/SwapMath.sol @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.7.6; +pragma abicoder v2; + +import "./FullMath.sol"; +import "./SqrtPriceMath.sol"; + +// https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/SwapMath.sol +library SwapMath { + /// @notice Computes the result of swapping some amount in, or amount out, given the parameters of the swap + /// @dev The fee, plus the amount in, will never exceed the amount remaining if the swap's `amountSpecified` is positive + /// @param sqrtRatioCurrentX96 The current sqrt price of the pool + /// @param sqrtRatioTargetX96 The price that cannot be exceeded, from which the direction of the swap is inferred + /// @param liquidity The usable liquidity + /// @param amountRemaining How much input or output amount is remaining to be swapped in/out + /// @param feePips The fee taken from the input amount, expressed in hundredths of a bip + /// @return sqrtRatioNextX96 The price after swapping the amount in/out, not to exceed the price target + /// @return amountIn The amount to be swapped in, of either token0 or token1, based on the direction of the swap + /// @return amountOut The amount to be received, of either token0 or token1, based on the direction of the swap + /// @return feeAmount The amount of input that will be taken as a fee + function computeSwapStep( + uint160 sqrtRatioCurrentX96, + uint160 sqrtRatioTargetX96, + uint128 liquidity, + int256 amountRemaining, + uint24 feePips + ) + internal + pure + returns ( + uint160 sqrtRatioNextX96, + uint256 amountIn, + uint256 amountOut, + uint256 feeAmount + ) + { + bool zeroForOne = sqrtRatioCurrentX96 >= sqrtRatioTargetX96; + bool exactIn = amountRemaining >= 0; + + { + if (exactIn) { + uint256 amountRemainingLessFee = FullMath.mulDiv(uint256(amountRemaining), 1e6 - feePips, 1e6); + amountIn = zeroForOne + ? SqrtPriceMath.getAmount0Delta(sqrtRatioTargetX96, sqrtRatioCurrentX96, liquidity, true) + : SqrtPriceMath.getAmount1Delta(sqrtRatioCurrentX96, sqrtRatioTargetX96, liquidity, true); + if (amountRemainingLessFee >= amountIn) sqrtRatioNextX96 = sqrtRatioTargetX96; + else + sqrtRatioNextX96 = SqrtPriceMath.getNextSqrtPriceFromInput( + sqrtRatioCurrentX96, + liquidity, + amountRemainingLessFee, + zeroForOne + ); + } else { + amountOut = zeroForOne + ? SqrtPriceMath.getAmount1Delta(sqrtRatioTargetX96, sqrtRatioCurrentX96, liquidity, false) + : SqrtPriceMath.getAmount0Delta(sqrtRatioCurrentX96, sqrtRatioTargetX96, liquidity, false); + if (uint256(-amountRemaining) >= amountOut) sqrtRatioNextX96 = sqrtRatioTargetX96; + else + sqrtRatioNextX96 = SqrtPriceMath.getNextSqrtPriceFromOutput( + sqrtRatioCurrentX96, + liquidity, + uint256(-amountRemaining), + zeroForOne + ); + } + } + + bool max = sqrtRatioTargetX96 == sqrtRatioNextX96; + + // get the input/output amounts + { + if (zeroForOne) { + amountIn = max && exactIn + ? amountIn + : SqrtPriceMath.getAmount0Delta(sqrtRatioNextX96, sqrtRatioCurrentX96, liquidity, true); + amountOut = max && !exactIn + ? amountOut + : SqrtPriceMath.getAmount1Delta(sqrtRatioNextX96, sqrtRatioCurrentX96, liquidity, false); + }else { + amountIn = max && exactIn + ? amountIn + : SqrtPriceMath.getAmount1Delta(sqrtRatioCurrentX96, sqrtRatioNextX96, liquidity, true); + amountOut = max && !exactIn + ? amountOut + : SqrtPriceMath.getAmount0Delta(sqrtRatioCurrentX96, sqrtRatioNextX96, liquidity, false); + } + } + + // cap the output amount to not exceed the remaining output amount + if (!exactIn && amountOut > uint256(-amountRemaining)) { + amountOut = uint256(-amountRemaining); + } + + if (exactIn && sqrtRatioNextX96 != sqrtRatioTargetX96) { + // we didn't reach the target, so take the remainder of the maximum input as fee + feeAmount = uint256(amountRemaining) - amountIn; + } else { + feeAmount = FullMath.mulDivRoundingUp(amountIn, feePips, 1e6 - feePips); + } + } +} \ No newline at end of file diff --git a/contracts/libraries/uniswap/TickBitmap.sol b/contracts/libraries/uniswap/TickBitmap.sol new file mode 100644 index 0000000..b0e4100 --- /dev/null +++ b/contracts/libraries/uniswap/TickBitmap.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.7.6; +pragma abicoder v2; + +import "./BitMath.sol"; + +interface IUniswapV3PoolBitmap { + function tickBitmap(int16 wordPosition) external view returns (uint256); +} + +struct TickNextWithWordQuery{ + address pool; + int24 tick; + int24 tickSpacing; + bool lte; +} + +// https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/TickBitmap.sol +library TickBitmap { + /// @notice Computes the position in the mapping where the initialized bit for a tick lives + /// @param tick The tick for which to compute the position + /// @return wordPos The key in the mapping containing the word in which the bit is stored + /// @return bitPos The bit position in the word where the flag is stored + function position(int24 tick) private pure returns (int16 wordPos, uint8 bitPos) { + wordPos = int16(tick >> 8); + bitPos = uint8(tick % 256); + } + + /// @notice Returns the next initialized tick contained in the same word (or adjacent word) as the tick that is either + /// to the left (less than or equal to) or right (greater than) of the given tick + /// @param _query.pool The Uniswap V3 pool to fetch the ticks BitMap + /// @param _query.tick The starting tick + /// @param _query.tickSpacing The spacing between usable ticks + /// @param _query.lte Whether to search for the next initialized tick to the left (less than or equal to the starting tick) + /// @return next The next initialized or uninitialized tick up to 256 ticks away from the current tick + /// @return initialized Whether the next tick is initialized, as the function only searches within up to 256 ticks + function nextInitializedTickWithinOneWord(TickNextWithWordQuery memory _query) internal view returns (int24 next, bool initialized) { + int24 compressed = _query.tick / _query.tickSpacing; + if (_query.tick < 0 && _query.tick % _query.tickSpacing != 0) compressed--; // round towards negative infinity + + if (_query.lte) { + (int16 wordPos, uint8 bitPos) = position(compressed); + // all the 1s at or to the right of the current bitPos + uint256 mask = (1 << bitPos) - 1 + (1 << bitPos); + uint256 masked = IUniswapV3PoolBitmap(_query.pool).tickBitmap(wordPos) & mask; + + // if there are no initialized ticks to the right of or at the current tick, return rightmost in the word + initialized = masked != 0; + // overflow/underflow is possible, but prevented externally by limiting both tickSpacing and tick + next = initialized + ? (compressed - int24(bitPos - BitMath.mostSignificantBit(masked))) * _query.tickSpacing + : (compressed - int24(bitPos)) * _query.tickSpacing; + } else { + // start from the word of the next tick, since the current tick state doesn't matter + (int16 wordPos, uint8 bitPos) = position(compressed + 1); + // all the 1s at or to the left of the bitPos + uint256 mask = ~((1 << bitPos) - 1); + uint256 masked = IUniswapV3PoolBitmap(_query.pool).tickBitmap(wordPos) & mask; + + // if there are no initialized ticks to the left of the current tick, return leftmost in the word + initialized = masked != 0; + // overflow/underflow is possible, but prevented externally by limiting both tickSpacing and tick + next = initialized + ? (compressed + 1 + int24(BitMath.leastSignificantBit(masked) - bitPos)) *_query. tickSpacing + : (compressed + 1 + int24(type(uint8).max - bitPos)) * _query.tickSpacing; + } + } +} \ No newline at end of file diff --git a/contracts/libraries/uniswap/TickMath.sol b/contracts/libraries/uniswap/TickMath.sol new file mode 100644 index 0000000..60742fe --- /dev/null +++ b/contracts/libraries/uniswap/TickMath.sol @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.7.6; +pragma abicoder v2; + +// https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/TickMath.sol +library TickMath { + /// @dev The minimum tick that may be passed to #getSqrtRatioAtTick computed from log base 1.0001 of 2**-128 + int24 internal constant MIN_TICK = -887272; + /// @dev The maximum tick that may be passed to #getSqrtRatioAtTick computed from log base 1.0001 of 2**128 + int24 internal constant MAX_TICK = -MIN_TICK; + + /// @dev The minimum value that can be returned from #getSqrtRatioAtTick. Equivalent to getSqrtRatioAtTick(MIN_TICK) + uint160 internal constant MIN_SQRT_RATIO = 4295128739; + /// @dev The maximum value that can be returned from #getSqrtRatioAtTick. Equivalent to getSqrtRatioAtTick(MAX_TICK) + uint160 internal constant MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342; + + /// @notice Calculates sqrt(1.0001^tick) * 2^96 + /// @dev Throws if |tick| > max tick + /// @param tick The input tick for the above formula + /// @return sqrtPriceX96 A Fixed point Q64.96 number representing the sqrt of the ratio of the two assets (token1/token0) + /// at the given tick + function getSqrtRatioAtTick(int24 tick) internal pure returns (uint160 sqrtPriceX96) { + uint256 absTick = tick < 0 ? uint256(-int256(tick)) : uint256(int256(tick)); + require(absTick <= uint256(MAX_TICK), 'T'); + + uint256 ratio = absTick & 0x1 != 0 ? 0xfffcb933bd6fad37aa2d162d1a594001 : 0x100000000000000000000000000000000; + if (absTick & 0x2 != 0) ratio = (ratio * 0xfff97272373d413259a46990580e213a) >> 128; + if (absTick & 0x4 != 0) ratio = (ratio * 0xfff2e50f5f656932ef12357cf3c7fdcc) >> 128; + if (absTick & 0x8 != 0) ratio = (ratio * 0xffe5caca7e10e4e61c3624eaa0941cd0) >> 128; + if (absTick & 0x10 != 0) ratio = (ratio * 0xffcb9843d60f6159c9db58835c926644) >> 128; + if (absTick & 0x20 != 0) ratio = (ratio * 0xff973b41fa98c081472e6896dfb254c0) >> 128; + if (absTick & 0x40 != 0) ratio = (ratio * 0xff2ea16466c96a3843ec78b326b52861) >> 128; + if (absTick & 0x80 != 0) ratio = (ratio * 0xfe5dee046a99a2a811c461f1969c3053) >> 128; + if (absTick & 0x100 != 0) ratio = (ratio * 0xfcbe86c7900a88aedcffc83b479aa3a4) >> 128; + if (absTick & 0x200 != 0) ratio = (ratio * 0xf987a7253ac413176f2b074cf7815e54) >> 128; + if (absTick & 0x400 != 0) ratio = (ratio * 0xf3392b0822b70005940c7a398e4b70f3) >> 128; + if (absTick & 0x800 != 0) ratio = (ratio * 0xe7159475a2c29b7443b29c7fa6e889d9) >> 128; + if (absTick & 0x1000 != 0) ratio = (ratio * 0xd097f3bdfd2022b8845ad8f792aa5825) >> 128; + if (absTick & 0x2000 != 0) ratio = (ratio * 0xa9f746462d870fdf8a65dc1f90e061e5) >> 128; + if (absTick & 0x4000 != 0) ratio = (ratio * 0x70d869a156d2a1b890bb3df62baf32f7) >> 128; + if (absTick & 0x8000 != 0) ratio = (ratio * 0x31be135f97d08fd981231505542fcfa6) >> 128; + if (absTick & 0x10000 != 0) ratio = (ratio * 0x9aa508b5b7a84e1c677de54f3e99bc9) >> 128; + if (absTick & 0x20000 != 0) ratio = (ratio * 0x5d6af8dedb81196699c329225ee604) >> 128; + if (absTick & 0x40000 != 0) ratio = (ratio * 0x2216e584f5fa1ea926041bedfe98) >> 128; + if (absTick & 0x80000 != 0) ratio = (ratio * 0x48a170391f7dc42444e8fa2) >> 128; + + if (tick > 0) ratio = type(uint256).max / ratio; + + // this divides by 1<<32 rounding up to go from a Q128.128 to a Q128.96. + // we then downcast because we know the result always fits within 160 bits due to our tick input constraint + // we round up in the division so getTickAtSqrtRatio of the output price is always consistent + sqrtPriceX96 = uint160((ratio >> 32) + (ratio % (1 << 32) == 0 ? 0 : 1)); + } + + /// @notice Calculates the greatest tick value such that getRatioAtTick(tick) <= ratio + /// @dev Throws in case sqrtPriceX96 < MIN_SQRT_RATIO, as MIN_SQRT_RATIO is the lowest value getRatioAtTick may + /// ever return. + /// @param sqrtPriceX96 The sqrt ratio for which to compute the tick as a Q64.96 + /// @return tick The greatest tick for which the ratio is less than or equal to the input ratio + function getTickAtSqrtRatio(uint160 sqrtPriceX96) internal pure returns (int24 tick) { + // second inequality must be < because the price can never reach the price at the max tick + require(sqrtPriceX96 >= MIN_SQRT_RATIO && sqrtPriceX96 < MAX_SQRT_RATIO, 'R'); + uint256 ratio = uint256(sqrtPriceX96) << 32; + + uint256 r = ratio; + uint256 msb = 0; + + assembly { + let f := shl(7, gt(r, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)) + msb := or(msb, f) + r := shr(f, r) + } + assembly { + let f := shl(6, gt(r, 0xFFFFFFFFFFFFFFFF)) + msb := or(msb, f) + r := shr(f, r) + } + assembly { + let f := shl(5, gt(r, 0xFFFFFFFF)) + msb := or(msb, f) + r := shr(f, r) + } + assembly { + let f := shl(4, gt(r, 0xFFFF)) + msb := or(msb, f) + r := shr(f, r) + } + assembly { + let f := shl(3, gt(r, 0xFF)) + msb := or(msb, f) + r := shr(f, r) + } + assembly { + let f := shl(2, gt(r, 0xF)) + msb := or(msb, f) + r := shr(f, r) + } + assembly { + let f := shl(1, gt(r, 0x3)) + msb := or(msb, f) + r := shr(f, r) + } + assembly { + let f := gt(r, 0x1) + msb := or(msb, f) + } + + if (msb >= 128) r = ratio >> (msb - 127); + else r = ratio << (127 - msb); + + int256 log_2 = (int256(msb) - 128) << 64; + + assembly { + r := shr(127, mul(r, r)) + let f := shr(128, r) + log_2 := or(log_2, shl(63, f)) + r := shr(f, r) + } + assembly { + r := shr(127, mul(r, r)) + let f := shr(128, r) + log_2 := or(log_2, shl(62, f)) + r := shr(f, r) + } + assembly { + r := shr(127, mul(r, r)) + let f := shr(128, r) + log_2 := or(log_2, shl(61, f)) + r := shr(f, r) + } + assembly { + r := shr(127, mul(r, r)) + let f := shr(128, r) + log_2 := or(log_2, shl(60, f)) + r := shr(f, r) + } + assembly { + r := shr(127, mul(r, r)) + let f := shr(128, r) + log_2 := or(log_2, shl(59, f)) + r := shr(f, r) + } + assembly { + r := shr(127, mul(r, r)) + let f := shr(128, r) + log_2 := or(log_2, shl(58, f)) + r := shr(f, r) + } + assembly { + r := shr(127, mul(r, r)) + let f := shr(128, r) + log_2 := or(log_2, shl(57, f)) + r := shr(f, r) + } + assembly { + r := shr(127, mul(r, r)) + let f := shr(128, r) + log_2 := or(log_2, shl(56, f)) + r := shr(f, r) + } + assembly { + r := shr(127, mul(r, r)) + let f := shr(128, r) + log_2 := or(log_2, shl(55, f)) + r := shr(f, r) + } + assembly { + r := shr(127, mul(r, r)) + let f := shr(128, r) + log_2 := or(log_2, shl(54, f)) + r := shr(f, r) + } + assembly { + r := shr(127, mul(r, r)) + let f := shr(128, r) + log_2 := or(log_2, shl(53, f)) + r := shr(f, r) + } + assembly { + r := shr(127, mul(r, r)) + let f := shr(128, r) + log_2 := or(log_2, shl(52, f)) + r := shr(f, r) + } + assembly { + r := shr(127, mul(r, r)) + let f := shr(128, r) + log_2 := or(log_2, shl(51, f)) + r := shr(f, r) + } + assembly { + r := shr(127, mul(r, r)) + let f := shr(128, r) + log_2 := or(log_2, shl(50, f)) + } + + int256 log_sqrt10001 = log_2 * 255738958999603826347141; // 128.128 number + + int24 tickLow = int24((log_sqrt10001 - 3402992956809132418596140100660247210) >> 128); + int24 tickHi = int24((log_sqrt10001 + 291339464771989622907027621153398088495) >> 128); + + tick = tickLow == tickHi ? tickLow : getSqrtRatioAtTick(tickHi) <= sqrtPriceX96 ? tickHi : tickLow; + } +} \ No newline at end of file diff --git a/contracts/libraries/uniswap/UnsafeMath.sol b/contracts/libraries/uniswap/UnsafeMath.sol new file mode 100644 index 0000000..63e82b1 --- /dev/null +++ b/contracts/libraries/uniswap/UnsafeMath.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.7.6; +pragma abicoder v2; + +// https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/UnsafeMath.sol +library UnsafeMath { + /// @notice Returns ceil(x / y) + /// @dev division by 0 has unspecified behavior, and must be checked externally + /// @param x The dividend + /// @param y The divisor + /// @return z The quotient, ceil(x / y) + function divRoundingUp(uint256 x, uint256 y) internal pure returns (uint256 z) { + assembly { + z := add(div(x, y), gt(mod(x, y), 0)) + } + } +} \ No newline at end of file diff --git a/interfaces/uniswap/IV3Pool.sol b/interfaces/uniswap/IV3Pool.sol index 0429e4b..6e4a6f4 100644 --- a/interfaces/uniswap/IV3Pool.sol +++ b/interfaces/uniswap/IV3Pool.sol @@ -1,8 +1,14 @@ // SPDX-License-Identifier: GPL-2.0-or-later -pragma solidity >=0.7.5; +pragma solidity 0.8.10; pragma abicoder v2; interface IUniswapV3Pool { function slot0() external view returns (uint160 sqrtPriceX96, int24, uint16, uint16, uint16, uint8, bool); function liquidity() external view returns (uint128); + function tickBitmap(int16 wordPosition) external view returns (uint256); + function tickSpacing() external view returns (int24); + function fee() external view returns (uint24); + function token0() external view returns (address); + function token1() external view returns (address); + function ticks(int24 tick) external view returns (uint128 liquidityGross, int128 liquidityNet, uint256 feeGrowthOutside0X128, uint256 feeGrowthOutside1X128, int56 tickCumulativeOutside, uint160 secondsPerLiquidityOutsideX128, uint32 secondsOutside, bool initialized); } \ No newline at end of file diff --git a/interfaces/uniswap/IV3Simulator.sol b/interfaces/uniswap/IV3Simulator.sol new file mode 100644 index 0000000..f92a889 --- /dev/null +++ b/interfaces/uniswap/IV3Simulator.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity 0.8.10; +pragma abicoder v2; + +interface IUniswapV3Simulator { + function simulateUniV3Swap(address _pool, address _token0, address _token1, bool _zeroForOne, uint24 _fee, uint256 _amountIn, uint256 _slippageAllowedBps) external view returns (uint256); +} \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index a843ba3..21833e9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -32,18 +32,21 @@ WBTC_WHALE = "0xbf72da2bd84c5170618fbe5914b0eca9638d5eb5" ## Contracts ## + @pytest.fixture def swapexecutor(): return OnChainSwapMainnet.deploy({"from": a[0]}) @pytest.fixture def pricer(): - return OnChainPricingMainnet.deploy({"from": a[0]}) + univ3simulator = UniV3SwapSimulator.deploy({"from": a[0]}) + return OnChainPricingMainnet.deploy(univ3simulator.address, {"from": a[0]}) @pytest.fixture def lenient_contract(): ## NOTE: We have 5% slippage on this one - c = OnChainPricingMainnetLenient.deploy({"from": a[0]}) + univ3simulator = UniV3SwapSimulator.deploy({"from": a[0]}) + c = OnChainPricingMainnetLenient.deploy(univ3simulator.address, {"from": a[0]}) c.setSlippage(499, {"from": accounts.at(c.TECH_OPS(), force=True)}) return c diff --git a/tests/on_chain_pricer/test_univ3_pricer_simu.py b/tests/on_chain_pricer/test_univ3_pricer_simu.py new file mode 100644 index 0000000..b3bcc10 --- /dev/null +++ b/tests/on_chain_pricer/test_univ3_pricer_simu.py @@ -0,0 +1,31 @@ +import brownie +from brownie import * +import pytest + +""" + simulateUniV3Swap quote for token A swapped to token B directly: A - > B +""" +def test_simu_univ3_swap(oneE18, weth, usdc, pricer): + ## 1e18 + sell_count = 10; + sell_amount = sell_count * oneE18 + + ## minimum quote for ETH in USDC(1e6) ## Rip ETH price + p = sell_count * 900 * 1000000 + quote = pricer.simulateUniV3Swap(pricer.uniV3Simulator(), weth.address, sell_amount, usdc.address, 500, 100) + + assert quote >= p + +""" + simulateUniV3Swap quote for token A swapped to token B directly: A - > B +""" +def test_simu_univ3_swap2(oneE18, weth, wbtc, pricer): + ## 1e8 + sell_count = 10; + sell_amount = sell_count * 100000000 + + ## minimum quote for BTC in ETH(1e18) ## Rip ETH price + p = sell_count * 14 * oneE18 + quote = pricer.simulateUniV3Swap(pricer.uniV3Simulator(), wbtc.address, sell_amount, weth.address, 500, 100) + + assert quote >= p \ No newline at end of file From 7bacd84dbcec9cbfe732f5c196294b21064e5452 Mon Sep 17 00:00:00 2001 From: rayeaster Date: Mon, 18 Jul 2022 23:43:56 +0800 Subject: [PATCH 03/53] add Uniswap V3 sort by check in-range liquidity first --- contracts/OnChainPricingMainnet.sol | 154 +++++++++++------- contracts/UniV3SwapSimulator.sol | 91 ++++++++++- contracts/libraries/uniswap/SwapMath.sol | 35 ++-- interfaces/uniswap/IV3Simulator.sol | 19 +++ tests/conftest.py | 5 + tests/on_chain_pricer/test_balancer_pricer.py | 21 ++- .../test_bribe_tokens_supported.py | 13 ++ .../test_swap_exec_on_chain.py | 12 +- tests/on_chain_pricer/test_univ3_pricer.py | 12 +- .../on_chain_pricer/test_univ3_pricer_simu.py | 22 ++- 10 files changed, 291 insertions(+), 93 deletions(-) diff --git a/contracts/OnChainPricingMainnet.sol b/contracts/OnChainPricingMainnet.sol index 2e5ebe2..5a09bda 100644 --- a/contracts/OnChainPricingMainnet.sol +++ b/contracts/OnChainPricingMainnet.sol @@ -60,7 +60,7 @@ contract OnChainPricingMainnet { address public constant UNIV3_QUOTER = 0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6; bytes32 public constant UNIV3_POOL_INIT_CODE_HASH = 0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54; address public constant UNIV3_FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984; - uint24[3] univ3_fees = [uint24(100), 500, 3000];//, 10000]; //skip last fee for exotic tokens + uint24[4] univ3_fees = [uint24(100), 500, 3000, 10000]; // BalancerV2 Vault address public constant BALANCERV2_VAULT = 0xBA12222222228d8Ba445958a75a0704d566BF2C8; @@ -252,58 +252,95 @@ contract OnChainPricingMainnet { /// === UNIV3 === /// - /// @dev simulate Uniswap V3 swap using its tick-based math for given parameters - /// @dev check library UniV3SwapTick for more - function simulateUniV3Swap(address _simulator, address tokenIn, uint256 amountIn, address tokenOut, uint24 _fee, uint256 _slippageAllowedBps) public view returns (uint256){ - (address token0, address token1, bool token0Price) = _ifUniV3Token0Price(tokenIn, tokenOut); - address _pool = _getUniV3PoolAddress(token0, token1, _fee); - - { - // heuristic check0: ensure the pool [exist] and properly initiated with valid in-range liquidity - if (!_pool.isContract() || IUniswapV3Pool(_pool).liquidity() == 0) { - return 0; - } - } + /// @dev explore Uniswap V3 pools to check if there is a chance to resolve the swap with in-range liquidity (i.e., without crossing ticks) + /// @dev check helper UniV3SwapSimulator for more + /// @return maximum output (with current in-range liquidity & spot price) and according pool fee + function sortUniV3Pools(address tokenIn, uint256 amountIn, address tokenOut) public view returns (uint256, uint24){ + uint256 feeTypes = univ3_fees.length; + uint256 _maxInRangeQuote; + uint24 _maxInRangeFee; { - uint256 _t0Balance = IERC20(token0).balanceOf(_pool); - uint256 _t1Balance = IERC20(token1).balanceOf(_pool); - - // heuristic check1: ensure the pool tokenIn reserve makes sense in terms of [amountIn] - if ((token0Price? _t0Balance : _t1Balance) <= amountIn){ - return 0; - } + for (uint256 i = 0; i < feeTypes;){ + uint24 _fee = univ3_fees[i]; + bool _crossTick; + uint256 _outAmt; + { + // in-range swap check: find out whether the swap within current liquidity would move the price across next tick + (bool _outOfInRange, uint256 _outputAmount) = _checkUniV3InRangeLiquidity(tokenIn, tokenOut, amountIn, _fee, uniV3SimSlippageBps); + _crossTick = _outOfInRange; + _outAmt = _outputAmount; + } + { + // unfortunately we need to do a full simulation to cross ticks + if (_crossTick){ + _outAmt = simulateUniV3Swap(tokenIn, amountIn, tokenOut, _fee, uniV3SimSlippageBps); + } + } + { + if (_outAmt > _maxInRangeQuote){ + _maxInRangeQuote = _outAmt; + _maxInRangeFee = _fee; + } + } + unchecked { ++i; } + } } - return IUniswapV3Simulator(_simulator).simulateUniV3Swap(_pool, token0, token1, token0Price, _fee, amountIn, _slippageAllowedBps); + return (_maxInRangeQuote, _maxInRangeFee); } - /// @dev Given the address of the input token & amount & the output token - /// @return the quote for it - function getUniV3Price(address tokenIn, uint256 amountIn, address tokenOut) public returns (uint256) { - uint256 quoteRate; - + /// @dev internal function to avoid stack too deap for Uniswap V3 pool in-range liquidity check + function _checkUniV3InRangeLiquidity(address tokenIn, address tokenOut, uint256 amountIn, uint24 _fee, uint256 _uniV3SimSlippageBps) internal view returns (bool, uint256){ (address token0, address token1, bool token0Price) = _ifUniV3Token0Price(tokenIn, tokenOut); - uint256 feeTypes = univ3_fees.length; - for (uint256 i = 0; i < feeTypes; ){ - //filter out disqualified pools to save gas on quoter swap query - { - uint256 rate = simulateUniV3Swap(uniV3Simulator, tokenIn, amountIn, tokenOut, univ3_fees[i], uniV3SimSlippageBps);//_getUniV3Rate(token0, token1, univ3_fees[i], token0Price, amountIn) - if (rate > 0){ - //uint256 quote = _getUniV3QuoterQuery(tokenIn, tokenOut, univ3_fees[i], amountIn);// skip this "call and revert" gas-consuming method - quoteRate = rate > quoteRate? rate : quoteRate; - } + { + address _pool = _getUniV3PoolAddress(token0, token1, _fee); + if (!_pool.isContract() || IUniswapV3Pool(_pool).liquidity() <= 0) { + return (false, 0); + } + UniV3SortPoolQuery memory _sortQuery = UniV3SortPoolQuery(_pool, token0, token1, _fee, amountIn, token0Price, _uniV3SimSlippageBps); + return IUniswapV3Simulator(uniV3Simulator).checkInRangeLiquidity(_sortQuery); + } + } + + /// @dev internal function for a basic sanity check Uniswap V3 pool existence and balances + /// @return true if basic check pass otherwise false + function _checkUniV3PoolExistenceAndBalances(address _pool, uint128 _liq, address _token0, address _token1, bool token0Price, uint256 amountIn) internal view returns (bool, uint256, uint256) { + + { + // heuristic check0: ensure the pool [exist] and properly initiated with valid in-range liquidity + if (!_pool.isContract() || _liq == 0) { + return (false, 0, 0); } - - unchecked { ++i; } } - return quoteRate; + { + uint256 _t0Balance = IERC20(_token0).balanceOf(_pool); + uint256 _t1Balance = IERC20(_token1).balanceOf(_pool); + + // heuristic check1: ensure the pool tokenIn reserve makes sense in terms of [amountIn], i.e., the pool is liquid compared to swap amount + return ((token0Price? _t0Balance : _t1Balance) > amountIn, _t0Balance, _t1Balance); + } + } + + /// @dev simulate Uniswap V3 swap using its tick-based math for given parameters + /// @dev check helper UniV3SwapSimulator for more + function simulateUniV3Swap(address tokenIn, uint256 amountIn, address tokenOut, uint24 _fee, uint256 _slippageAllowedBps) public view returns (uint256){ + (address token0, address token1, bool token0Price) = _ifUniV3Token0Price(tokenIn, tokenOut); + address _pool = _getUniV3PoolAddress(token0, token1, _fee); + return IUniswapV3Simulator(uniV3Simulator).simulateUniV3Swap(_pool, token0, token1, token0Price, _fee, amountIn, _slippageAllowedBps); + } + + /// @dev Given the address of the input token & amount & the output token + /// @return the quote for it + function getUniV3Price(address tokenIn, uint256 amountIn, address tokenOut) public view returns (uint256) { + (uint256 _maxInRangeQuote, uint24 _maxPoolFee) = sortUniV3Pools(tokenIn, amountIn, tokenOut); + return _maxInRangeQuote; } /// @dev Given the address of the input token & amount & the output token & connector token in between (input token ---> connector token ---> output token) /// @return the quote for it - function getUniV3PriceWithConnector(address tokenIn, uint256 amountIn, address tokenOut, address connectorToken) public returns (uint256) { + function getUniV3PriceWithConnector(address tokenIn, uint256 amountIn, address tokenOut, address connectorToken) public view returns (uint256) { uint256 connectorAmount = getUniV3Price(tokenIn, amountIn, connectorToken); if (connectorAmount > 0){ return getUniV3Price(connectorToken, connectorAmount, tokenOut); @@ -313,6 +350,7 @@ contract OnChainPricingMainnet { } /// @dev query swap result from Uniswap V3 quoter for given tokenIn -> tokenOut with amountIn & fee + /// @dev this "call and revert" method consumes tons of gas function _getUniV3QuoterQuery(address tokenIn, address tokenOut, uint24 fee, uint256 amountIn) internal returns (uint256){ uint256 quote = IV3Quoter(UNIV3_QUOTER).quoteExactInputSingle(tokenIn, tokenOut, fee, amountIn, 0); return quote; @@ -328,33 +366,26 @@ contract OnChainPricingMainnet { /// @dev with trade amount & indicator if token0 pricing required (token1/token0 e.g., token0 -> token1) /// @dev note there are some heuristic checks around the price like pool reserve should satisfy the swap amount /// @return the current price in V3 for it - function _getUniV3Rate(address token0, address token1, uint24 fee, bool token0Price, uint256 amountIn) internal view returns (uint256) { + function _getUniV3RateHeuristic(address token0, address token1, uint24 fee, bool token0Price, uint256 amountIn) internal view returns (uint256) { // heuristic check0: ensure the pool [exist] and properly initiated with valid in-range liquidity - address pool = _getUniV3PoolAddress(token0, token1, fee); - if (!pool.isContract() || IUniswapV3Pool(pool).liquidity() == 0) { - return 0; - } - - uint256 _t0Balance = IERC20(token0).balanceOf(pool); - uint256 _t1Balance = IERC20(token1).balanceOf(pool); - - // heuristic check1: ensure the pool tokenIn reserve makes sense in terms of [amountIn] - if ((token0Price? _t0Balance : _t1Balance) <= amountIn){ - return 0; - } + address pool = _getUniV3PoolAddress(token0, token1, fee); + + //filter out disqualified pools to save gas on quoter swap query + (bool _basicCheck, uint256 _t0Balance, uint256 _t1Balance) = _checkUniV3PoolExistenceAndBalances(pool, IUniswapV3Pool(pool).liquidity(), token0, token1, token0Price, amountIn); + if (!_basicCheck) return 0; uint256 _t0Decimals = 10 ** IERC20Metadata(token0).decimals(); uint256 _t1Decimals = 10 ** IERC20Metadata(token1).decimals(); - // heuristic check2: ensure the pool tokenOut reserve makes sense in terms of the [amountOutput based on slot0 price] - uint256 rate = _queryUniV3PriceWithSlot(token0, token1, pool, token0Price, _t0Decimals, _t1Decimals); - uint256 amountOutput = rate * amountIn * (token0Price? _t1Decimals : _t0Decimals) / (token0Price? _t0Decimals : _t1Decimals) / 1e18; + // Heuristically reserve with spot price check: ensure the pool tokenOut reserve makes sense in terms of thespot price [amountOutput based on slot0 price] + (uint256 rate, uint256 amountOutput) = _getOutputWithSlot0Price(token0, token1, pool, token0Price, _t0Decimals, _t1Decimals, amountIn); if ((token0Price? _t1Balance : _t0Balance) <= amountOutput){ return 0; } - // heuristic check3: ensure the pool [reserve comparison is consistent with the slot0 price comparison], i.e., asset in less amount should be more expensive in AMM pool + // Heuristically reserves comparison check: ensure the pool [reserve comparison is consistent with the slot0 price comparison], + // i.e., asset in less amount should be more expensive in AMM pool bool token0MoreExpensive = _compareUniV3Tokens(token0Price, rate); bool token0MoreReserved = _compareUniV3TokenReserves(_t0Balance, _t1Balance, _t0Decimals, _t1Decimals); if (token0MoreExpensive == token0MoreReserved){ @@ -364,6 +395,13 @@ contract OnChainPricingMainnet { return amountOutput; } + /// @dev calculate output amount according to Uniswap V3 spot price (slot0) + function _getOutputWithSlot0Price(address token0, address token1, address pool, bool token0Price, uint256 _t0Decimals, uint256 _t1Decimals, uint256 amountIn) internal view returns (uint256, uint256) { + uint256 rate = _queryUniV3PriceWithSlot(token0, token1, pool, token0Price, _t0Decimals, _t1Decimals); + uint256 amountOutput = rate * amountIn * (token0Price? _t1Decimals : _t0Decimals) / (token0Price? _t0Decimals : _t1Decimals) / 1e18; + return (rate, amountOutput); + } + /// @dev query current price from V3 pool interface(slot0) with given pool & token0 & token1 /// @dev and indicator if token0 pricing required (token1/token0 e.g., token0 -> token1) /// @return the price of required token scaled with 1e18 @@ -442,10 +480,10 @@ contract OnChainPricingMainnet { require(_outTokenIdx < tokens.length, '!outBAL'); /// Balancer math for spot price of tokenIn -> tokenOut: weighted value(number * price) relation should be kept - _pIn2Out = (_weights[_inTokenIdx] * balances[_outTokenIdx] / _outDecimals) / (_weights[_outTokenIdx] * balances[_inTokenIdx] / _inDecimals); + _pIn2Out = 1e18 * (_weights[_inTokenIdx] * balances[_outTokenIdx] / _outDecimals) / (_weights[_outTokenIdx] * balances[_inTokenIdx] / _inDecimals); } - return amountIn * _pIn2Out * _outDecimals / _inDecimals; + return amountIn * _pIn2Out * _outDecimals / _inDecimals / 1e18; } function _findTokenInBalancePool(address _token, address[] memory _tokens) internal view returns (uint256){ diff --git a/contracts/UniV3SwapSimulator.sol b/contracts/UniV3SwapSimulator.sol index bc1843a..2571528 100644 --- a/contracts/UniV3SwapSimulator.sol +++ b/contracts/UniV3SwapSimulator.sol @@ -5,7 +5,23 @@ pragma abicoder v2; import "./libraries/uniswap/SwapMath.sol"; import "./libraries/uniswap/TickBitmap.sol"; import "./libraries/uniswap/TickMath.sol"; +import "./libraries/uniswap/FullMath.sol"; import "./libraries/uniswap/LiquidityMath.sol"; +import "./libraries/uniswap/SqrtPriceMath.sol"; + +struct UniV3SortPoolQuery{ + address _pool; + address _token0; + address _token1; + uint24 _fee; + uint256 amountIn; + bool zeroForOne; + uint256 _slippageAllowedBps; +} + +interface IERC20 { + function balanceOf(address _owner) external view returns (uint256); +} interface IUniswapV3PoolSwapTick { function slot0() external view returns (uint160 sqrtPriceX96, int24, uint16, uint16, uint16, uint8, bool); @@ -29,13 +45,15 @@ contract UniV3SwapSimulator { using LowGasSafeMath for int256; using SafeCast for uint256; using SafeCast for int256; + + uint256 public constant MAX_BPS = 10000; /// @dev View function which aims to simplify Uniswap V3 swap logic (no oracle/fee update, etc) to /// @dev estimate the expected output for given swap parameters and slippage /// @dev simplified version of https://github.com/Uniswap/v3-core/blob/main/contracts/UniswapV3Pool.sol#L596 /// @return simulated output token amount using Uniswap V3 tick-based math function simulateUniV3Swap(address _pool, address _token0, address _token1, bool _zeroForOne, uint24 _fee, uint256 _amountIn, uint256 _slippageAllowedBps) external view returns (uint256){ - require(_slippageAllowedBps < 10000, '!SLIP'); + require(_slippageAllowedBps < MAX_BPS, '!SLIP'); require(_slippageAllowedBps > 0, '!SLI0'); // Get current state of the pool @@ -47,7 +65,7 @@ contract UniV3SwapSimulator { { (uint160 _currentPX96, int24 _currentTick,,,,,) = IUniswapV3PoolSwapTick(_pool).slot0(); - _sqrtPriceLimitX96 = _zeroForOne? (_currentPX96 * (10000 - _slippageAllowedBps).toUint160() / 10000) : (_currentPX96 * (10000 + _slippageAllowedBps).toUint160() / 10000); + _sqrtPriceLimitX96 = _getLimitPriceFromSlippageAllowance(_zeroForOne, _currentPX96, _slippageAllowedBps); state = SwapStatus(_amountIn.toInt256(), _currentPX96, _currentTick, IUniswapV3PoolSwapTick(_pool).liquidity(), 0); } @@ -61,14 +79,48 @@ contract UniV3SwapSimulator { return uint256(state._amountCalculated); } + /// @dev allow caller to check if given amountIn would be satisfied with in-range liquidity + /// @return true if in-range liquidity is good for the quote otherwise false which means a full cross-ticks simulation required + function checkInRangeLiquidity(UniV3SortPoolQuery memory _sortQuery) public view returns (bool, uint256) { + uint128 _liq = IUniswapV3PoolSwapTick(_sortQuery._pool).liquidity(); + + // are we swapping in a liquid-enough pool? + if (_liq <= 0) { + return (false, 0); + } + + { + (uint160 _swapAfterPrice, uint160 _tickNextPrice, uint160 _currentPriceX96) = _findSwapPriceExactIn(_sortQuery, _liq); + bool _crossTick = _sortQuery.zeroForOne? (_swapAfterPrice <= _tickNextPrice) : (_swapAfterPrice >= _tickNextPrice); + if (_crossTick){ + return (true, 0); + } else{ + return (false, _getAmountOutputDelta(_swapAfterPrice, _currentPriceX96, _liq, _sortQuery.zeroForOne)); + } + } + } + + /// @dev retrieve next initialized tick for given Uniswap V3 pool + function _getNextInitializedTick(TickNextWithWordQuery memory _nextTickQuery) internal view returns (int24, bool, uint160) { + (int24 tickNext, bool initialized) = TickBitmap.nextInitializedTickWithinOneWord(_nextTickQuery); + uint160 sqrtPriceNextX96 = TickMath.getSqrtRatioAtTick(tickNext); + return (tickNext, initialized, sqrtPriceNextX96); + } + + /// @dev return calculated output amount in the Uniswap V3 pool for given price pair + /// @dev works for any swap that does not push the calculated next price past the price of the next initialized tick + /// @dev check SwapMath for details + function _getAmountOutputDelta(uint160 _nextPrice, uint160 _currentPrice, uint128 _liquidity, bool _zeroForOne) internal pure returns (uint256) { + return _zeroForOne? SqrtPriceMath.getAmount1Delta(_nextPrice, _currentPrice, _liquidity, false) : SqrtPriceMath.getAmount0Delta(_currentPrice, _nextPrice, _liquidity, false); + } + /// @dev swap step in the tick function _stepInTick(SwapStatus memory state, TickNextWithWordQuery memory _nextTickQuery, uint24 _fee, bool _zeroForOne, uint160 _sqrtPriceLimitX96) view internal{ /// Fetch NEXT-STEP tick to prepare for crossing - (int24 tickNext, bool initialized) = TickBitmap.nextInitializedTickWithinOneWord(_nextTickQuery); - uint160 sqrtPriceNextX96 = TickMath.getSqrtRatioAtTick(tickNext); + (int24 tickNext, bool initialized, uint160 sqrtPriceNextX96) = _getNextInitializedTick(_nextTickQuery); uint160 sqrtPriceStartX96 = state._sqrtPriceX96; - uint160 _targetPX96 = (_zeroForOne ? sqrtPriceNextX96 < _sqrtPriceLimitX96 : sqrtPriceNextX96 > _sqrtPriceLimitX96)? _sqrtPriceLimitX96 : sqrtPriceNextX96; + uint160 _targetPX96 = _getTargetPriceForSwapStep(_zeroForOne, sqrtPriceNextX96, _sqrtPriceLimitX96); /// Trying to perform in-tick swap { @@ -91,6 +143,35 @@ contract UniV3SwapSimulator { } } + function _findSwapPriceExactIn(UniV3SortPoolQuery memory _sortQuery, uint128 _liq) internal view returns (uint160, uint160, uint160) { + uint160 _tickNextPrice; + uint160 _swapAfterPrice; + (uint160 _currentPriceX96, int24 _tick,,,,,) = IUniswapV3PoolSwapTick(_sortQuery._pool).slot0(); + + { + TickNextWithWordQuery memory _nextTickQ = TickNextWithWordQuery(_sortQuery._pool, _tick, IUniswapV3PoolSwapTick(_sortQuery._pool).tickSpacing(), _sortQuery.zeroForOne); + (,,uint160 _nxtTkP) = _getNextInitializedTick(_nextTickQ); + _tickNextPrice = _nxtTkP; + } + + { + uint160 _targetPX96 = _getTargetPriceForSwapStep(_sortQuery.zeroForOne, _tickNextPrice, _getLimitPriceFromSlippageAllowance(_sortQuery.zeroForOne, _currentPriceX96, _sortQuery._slippageAllowedBps)); + SwapExactInParam memory _exactInParams = SwapExactInParam(_sortQuery.amountIn, _sortQuery._fee, _currentPriceX96, _targetPX96, _liq, _sortQuery.zeroForOne); + (uint256 _amtIn, uint160 _newPrice) = SwapMath._getExactInNextPrice(_exactInParams); + _swapAfterPrice = _newPrice; + } + + return (_swapAfterPrice, _tickNextPrice, _currentPriceX96); + } + + function _getLimitPriceFromSlippageAllowance(bool _zeroForOne, uint160 _currentPX96, uint256 _slippageAllowedBps) internal pure returns (uint160) { + return _zeroForOne? (_currentPX96 * (MAX_BPS - _slippageAllowedBps).toUint160() / uint160(MAX_BPS)) : (_currentPX96 * (MAX_BPS + _slippageAllowedBps).toUint160() / uint160(MAX_BPS)); + } + + function _getTargetPriceForSwapStep(bool _zeroForOne, uint160 sqrtPriceNextX96, uint160 _sqrtPriceLimitX96) internal pure returns (uint160) { + return (_zeroForOne ? sqrtPriceNextX96 < _sqrtPriceLimitX96 : sqrtPriceNextX96 > _sqrtPriceLimitX96)? _sqrtPriceLimitX96 : sqrtPriceNextX96; + } + function _swapCalculation(SwapStatus memory state, uint160 _targetPX96, uint24 _fee) internal view { (uint160 sqrtPriceX96, uint256 amountIn, uint256 amountOut, uint256 feeAmount) = SwapMath.computeSwapStep(state._sqrtPriceX96, _targetPX96, state._liquidity, state._amountSpecifiedRemaining, _fee); diff --git a/contracts/libraries/uniswap/SwapMath.sol b/contracts/libraries/uniswap/SwapMath.sol index 033366a..eb69bc6 100644 --- a/contracts/libraries/uniswap/SwapMath.sol +++ b/contracts/libraries/uniswap/SwapMath.sol @@ -5,6 +5,15 @@ pragma abicoder v2; import "./FullMath.sol"; import "./SqrtPriceMath.sol"; +struct SwapExactInParam{ + uint256 _amountIn; + uint24 _fee; + uint160 _currentPriceX96; + uint160 _targetPriceX96; + uint128 _liquidity; + bool _zeroForOne; +} + // https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/SwapMath.sol library SwapMath { /// @notice Computes the result of swapping some amount in, or amount out, given the parameters of the swap @@ -39,18 +48,10 @@ library SwapMath { { if (exactIn) { - uint256 amountRemainingLessFee = FullMath.mulDiv(uint256(amountRemaining), 1e6 - feePips, 1e6); - amountIn = zeroForOne - ? SqrtPriceMath.getAmount0Delta(sqrtRatioTargetX96, sqrtRatioCurrentX96, liquidity, true) - : SqrtPriceMath.getAmount1Delta(sqrtRatioCurrentX96, sqrtRatioTargetX96, liquidity, true); - if (amountRemainingLessFee >= amountIn) sqrtRatioNextX96 = sqrtRatioTargetX96; - else - sqrtRatioNextX96 = SqrtPriceMath.getNextSqrtPriceFromInput( - sqrtRatioCurrentX96, - liquidity, - amountRemainingLessFee, - zeroForOne - ); + SwapExactInParam memory _exactInParams = SwapExactInParam(uint256(amountRemaining), feePips, sqrtRatioCurrentX96, sqrtRatioTargetX96, liquidity, zeroForOne); + (uint256 _amtIn, uint160 _nextPrice) = _getExactInNextPrice(_exactInParams); + amountIn = _amtIn; + sqrtRatioNextX96 = _nextPrice; } else { amountOut = zeroForOne ? SqrtPriceMath.getAmount1Delta(sqrtRatioTargetX96, sqrtRatioCurrentX96, liquidity, false) @@ -99,4 +100,14 @@ library SwapMath { feeAmount = FullMath.mulDivRoundingUp(amountIn, feePips, 1e6 - feePips); } } + + function _getExactInNextPrice(SwapExactInParam memory _exactInParams) internal pure returns (uint256, uint160){ + uint160 sqrtRatioNextX96; + uint256 amountRemainingLessFee = FullMath.mulDiv(_exactInParams._amountIn, 1e6 - (_exactInParams._fee), 1e6); + uint256 amountIn = _exactInParams._zeroForOne? SqrtPriceMath.getAmount0Delta(_exactInParams._targetPriceX96, _exactInParams._currentPriceX96, _exactInParams._liquidity, true) : + SqrtPriceMath.getAmount1Delta(_exactInParams._currentPriceX96, _exactInParams._targetPriceX96, _exactInParams._liquidity, true); + if (amountRemainingLessFee >= _exactInParams._amountIn) sqrtRatioNextX96 = _exactInParams._targetPriceX96; + else sqrtRatioNextX96 = SqrtPriceMath.getNextSqrtPriceFromInput(_exactInParams._currentPriceX96, _exactInParams._liquidity, amountRemainingLessFee, _exactInParams._zeroForOne); + return (amountIn, sqrtRatioNextX96); + } } \ No newline at end of file diff --git a/interfaces/uniswap/IV3Simulator.sol b/interfaces/uniswap/IV3Simulator.sol index f92a889..a0ba47c 100644 --- a/interfaces/uniswap/IV3Simulator.sol +++ b/interfaces/uniswap/IV3Simulator.sol @@ -2,6 +2,25 @@ pragma solidity 0.8.10; pragma abicoder v2; +// Uniswap V3 simulation query +struct TickNextWithWordQuery{ + address pool; + int24 tick; + int24 tickSpacing; + bool lte; +} + +struct UniV3SortPoolQuery{ + address _pool; + address _token0; + address _token1; + uint24 _fee; + uint256 amountIn; + bool zeroForOne; + uint256 _slippageAllowedBps; +} + interface IUniswapV3Simulator { function simulateUniV3Swap(address _pool, address _token0, address _token1, bool _zeroForOne, uint24 _fee, uint256 _amountIn, uint256 _slippageAllowedBps) external view returns (uint256); + function checkInRangeLiquidity(UniV3SortPoolQuery memory _sortQuery) external view returns (bool, uint256); } \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 21833e9..33668d9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,6 +20,7 @@ CVX_WHALE = "0xcf50b810e57ac33b91dcf525c6ddd9881b139332" DAI_WHALE = "0xe78388b4ce79068e89bf8aa7f218ef6b9ab0e9d0" AURA = "0xC0c293ce456fF0ED870ADd98a0828Dd4d2903DBF" +AURABAL = "0x616e8BfA43F920657B3497DBf40D6b1A02D4608d" BVE_CVX = "0xfd05D3C7fe2924020620A8bE4961bBaA747e6305" BVE_AURA = "0xBA485b556399123261a5F9c95d413B4f93107407" AURA_WHALE = "0x43B17088503F4CE1AED9fB302ED6BB51aD6694Fa" @@ -63,6 +64,10 @@ def processor(lenient_contract): def oneE18(): return 1000000000000000000 +@pytest.fixture +def aurabal(): + return interface.ERC20(AURABAL) + @pytest.fixture def ohm(): return interface.ERC20(OHM) diff --git a/tests/on_chain_pricer/test_balancer_pricer.py b/tests/on_chain_pricer/test_balancer_pricer.py index cf652db..19ee4e1 100644 --- a/tests/on_chain_pricer/test_balancer_pricer.py +++ b/tests/on_chain_pricer/test_balancer_pricer.py @@ -45,7 +45,7 @@ def test_get_balancer_price_with_connector(oneE18, wbtc, usdc, weth, pricer): """ getBalancerPrice quote for token A swapped to token B directly using given balancer pool: A - > B """ -def test_get_balancer_price2(oneE18, cvx, weth, pricer): +def test_get_balancer_price_nonexistence(oneE18, cvx, weth, pricer): ## 1e18 sell_amount = 100 * oneE18 @@ -81,7 +81,7 @@ def test_get_balancer_price_with_connector_analytical(oneE18, wbtc, usdc, weth, """ getBalancerPriceAnalytically quote for token A swapped to token B directly using given balancer pool: A - > B analytically """ -def test_get_balancer_price_ohm__analytical(oneE18, ohm, dai, pricer): +def test_get_balancer_price_ohm_analytical(oneE18, ohm, dai, pricer): ## 1e8 sell_count = 1000 sell_amount = sell_count * oneE18 @@ -89,5 +89,20 @@ def test_get_balancer_price_ohm__analytical(oneE18, ohm, dai, pricer): ## minimum quote for OHM in DAI(1e6) p = sell_count * 10 * oneE18 quote = pricer.getBalancerPriceAnalytically(ohm.address, sell_amount, dai.address) - assert quote >= p + assert quote >= p + +""" + getBalancerPrice quote for token A swapped to token B directly using given balancer pool: A - > B +""" +def test_get_balancer_price_aurabal_analytical(oneE18, aurabal, weth, pricer): + ## 1e18 + sell_count = 1000 + sell_amount = sell_count * oneE18 + + ## minimum quote for AURABAL in WETH(1e18) + p = sell_count * 0.006 * oneE18 + + ## there is a proper pool in Balancer for AURABAL in WETH + quote = pricer.getBalancerPriceAnalytically(aurabal.address, sell_amount, weth.address) + assert quote >= p \ No newline at end of file diff --git a/tests/on_chain_pricer/test_bribe_tokens_supported.py b/tests/on_chain_pricer/test_bribe_tokens_supported.py index 8172fd1..ba9389a 100644 --- a/tests/on_chain_pricer/test_bribe_tokens_supported.py +++ b/tests/on_chain_pricer/test_bribe_tokens_supported.py @@ -59,4 +59,17 @@ def test_are_bribes_supported(pricer, token): res = pricer.isPairSupported(token, WETH, AMOUNT).return_value assert res +@pytest.mark.parametrize("token", TOKENS_18_DECIMALS) +def test_bribes_get_optimal_quote(pricer, token): + """ + Given a bunch of tokens historically used as bribes, verifies the pricer will return non-zero value + We sell all to WETH which is pretty realistic + """ + + ## 1e18 for everything, even with insane slippage will still return non-zero which is sufficient at this time + AMOUNT = 1e18 + + res = pricer.findOptimalSwap(token, WETH, AMOUNT).return_value + assert res[1] > 0 + diff --git a/tests/on_chain_pricer/test_swap_exec_on_chain.py b/tests/on_chain_pricer/test_swap_exec_on_chain.py index af26615..526ac30 100644 --- a/tests/on_chain_pricer/test_swap_exec_on_chain.py +++ b/tests/on_chain_pricer/test_swap_exec_on_chain.py @@ -59,14 +59,14 @@ def test_swap_in_univ3_single(oneE18, wbtc_whale, wbtc, usdc, pricer, swapexecut ## minimum quote for WBTC in USDC(1e6) p = 1 * 15000 * 1000000 - quote = pricer.getUniV3Price(wbtc.address, sell_amount, usdc.address).return_value - assert quote >= p + quote = pricer.findOptimalSwap(wbtc.address, usdc.address, sell_amount).return_value + assert quote[1] >= p ## swap on chain slippageTolerance = 0.95 wbtc.transfer(swapexecutor.address, sell_amount, {'from': wbtc_whale}) - minOutput = quote * slippageTolerance + minOutput = quote[1] * slippageTolerance balBefore = usdc.balanceOf(wbtc_whale) swapexecutor.doOptimalSwapWithQuote(wbtc.address, usdc.address, sell_amount, (3, minOutput, [], [3000]), {'from': wbtc_whale}) balAfter = usdc.balanceOf(wbtc_whale) @@ -82,14 +82,14 @@ def test_swap_in_univ3(oneE18, wbtc_whale, wbtc, weth, usdc, pricer, swapexecuto ## minimum quote for WBTC in USDC(1e6) p = 1 * 15000 * 1000000 - quote = pricer.getUniV3PriceWithConnector(wbtc.address, sell_amount, usdc.address, weth.address).return_value - assert quote >= p + quote = pricer.findOptimalSwap(wbtc.address, usdc.address, sell_amount).return_value + assert quote[1] >= p ## swap on chain slippageTolerance = 0.95 wbtc.transfer(swapexecutor.address, sell_amount, {'from': wbtc_whale}) - minOutput = quote * slippageTolerance + minOutput = quote[1] * slippageTolerance ## encodedPath = swapexecutor.encodeUniV3TwoHop(wbtc.address, 500, weth.address, 500, usdc.address) balBefore = usdc.balanceOf(wbtc_whale) swapexecutor.doOptimalSwapWithQuote(wbtc.address, usdc.address, sell_amount, (4, minOutput, [], [500,500]), {'from': wbtc_whale}) diff --git a/tests/on_chain_pricer/test_univ3_pricer.py b/tests/on_chain_pricer/test_univ3_pricer.py index d29aa21..2a588a3 100644 --- a/tests/on_chain_pricer/test_univ3_pricer.py +++ b/tests/on_chain_pricer/test_univ3_pricer.py @@ -11,7 +11,7 @@ def test_get_univ3_price(oneE18, weth, usdc, pricer): ## minimum quote for ETH in USDC(1e6) ## Rip ETH price p = 1 * 900 * 1000000 - quote = pricer.getUniV3Price(weth.address, sell_amount, usdc.address).return_value + quote = pricer.getUniV3Price(weth.address, sell_amount, usdc.address) assert quote >= p @@ -22,9 +22,9 @@ def test_get_univ3_price_with_connector(oneE18, wbtc, usdc, weth, pricer): ## 1e8 sell_amount = 100 * 100000000 - ## minimum quote for WBTC in USDC(1e6) ## Rip ETH price + ## minimum quote for WBTC in USDC(1e6) p = 100 * 15000 * 1000000 - quoteWithConnector = pricer.getUniV3PriceWithConnector(wbtc.address, sell_amount, usdc.address, weth.address).return_value + quoteWithConnector = pricer.getUniV3PriceWithConnector(wbtc.address, sell_amount, usdc.address, weth.address) ## min price assert quoteWithConnector >= p @@ -36,9 +36,9 @@ def test_get_univ3_price_with_connector_stablecoin(oneE18, dai, usdc, weth, pric ## 1e18 sell_amount = 10000 * oneE18 - ## minimum quote for DAI in USDC(1e6) ## Rip ETH price + ## minimum quote for DAI in USDC(1e6) p = 10000 * 0.99 * 1000000 - quoteWithConnector = pricer.getUniV3PriceWithConnector(dai.address, sell_amount, usdc.address, weth.address).return_value + quoteWithConnector = pricer.getUniV3PriceWithConnector(dai.address, sell_amount, usdc.address, weth.address) ## min price - assert quoteWithConnector >= p \ No newline at end of file + assert quoteWithConnector >= p \ No newline at end of file diff --git a/tests/on_chain_pricer/test_univ3_pricer_simu.py b/tests/on_chain_pricer/test_univ3_pricer_simu.py index b3bcc10..0560f83 100644 --- a/tests/on_chain_pricer/test_univ3_pricer_simu.py +++ b/tests/on_chain_pricer/test_univ3_pricer_simu.py @@ -12,7 +12,7 @@ def test_simu_univ3_swap(oneE18, weth, usdc, pricer): ## minimum quote for ETH in USDC(1e6) ## Rip ETH price p = sell_count * 900 * 1000000 - quote = pricer.simulateUniV3Swap(pricer.uniV3Simulator(), weth.address, sell_amount, usdc.address, 500, 100) + quote = pricer.simulateUniV3Swap(weth.address, sell_amount, usdc.address, 500, 100) assert quote >= p @@ -26,6 +26,22 @@ def test_simu_univ3_swap2(oneE18, weth, wbtc, pricer): ## minimum quote for BTC in ETH(1e18) ## Rip ETH price p = sell_count * 14 * oneE18 - quote = pricer.simulateUniV3Swap(pricer.uniV3Simulator(), wbtc.address, sell_amount, weth.address, 500, 100) + quote = pricer.simulateUniV3Swap(wbtc.address, sell_amount, weth.address, 500, 100) - assert quote >= p \ No newline at end of file + assert quote >= p + +""" + sortUniV3Pools quote for stablecoin A swapped to stablecoin B which try for in-range swap before full-simulation + https://info.uniswap.org/#/tokens/0x6b175474e89094c44da98b954eedeac495271d0f +""" +def test_simu_univ3_swap_sort_pools(oneE18, dai, usdc, weth, pricer): + ## 1e18 + sell_amount = 10000 * oneE18 + + ## minimum quote for DAI in USDC(1e6) + p = 10000 * 0.999 * 1000000 + quoteInRangeAndFee = pricer.sortUniV3Pools(dai.address, sell_amount, usdc.address) + + ## min price + assert quoteInRangeAndFee[0] >= p + assert quoteInRangeAndFee[1] == 100 ## fee-0.01% pool got better quote than fee-0.05% pool \ No newline at end of file From de0aced5672dd23f0e4219ad6a5e4a48a4307798 Mon Sep 17 00:00:00 2001 From: rayeaster Date: Tue, 19 Jul 2022 10:51:56 +0800 Subject: [PATCH 04/53] gas-optimizing getBalancerPriceAnalytically --- contracts/OnChainPricingMainnet.sol | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/contracts/OnChainPricingMainnet.sol b/contracts/OnChainPricingMainnet.sol index 5a09bda..1475eb6 100644 --- a/contracts/OnChainPricingMainnet.sol +++ b/contracts/OnChainPricingMainnet.sol @@ -463,10 +463,7 @@ contract OnChainPricingMainnet { return 0; } - (address _pool,) = IBalancerV2Vault(BALANCERV2_VAULT).getPool(poolId); - - uint256 _inDecimals = 10 ** IERC20Metadata(tokenIn).decimals(); - uint256 _outDecimals = 10 ** IERC20Metadata(tokenOut).decimals(); + address _pool = getAddressFromBytes32(poolId); uint256 _pIn2Out; { @@ -480,13 +477,13 @@ contract OnChainPricingMainnet { require(_outTokenIdx < tokens.length, '!outBAL'); /// Balancer math for spot price of tokenIn -> tokenOut: weighted value(number * price) relation should be kept - _pIn2Out = 1e18 * (_weights[_inTokenIdx] * balances[_outTokenIdx] / _outDecimals) / (_weights[_outTokenIdx] * balances[_inTokenIdx] / _inDecimals); + _pIn2Out = 1e18 * (_weights[_inTokenIdx] * balances[_outTokenIdx]) / (_weights[_outTokenIdx] * balances[_inTokenIdx]); // _outDecimals / _inDecimals } - return amountIn * _pIn2Out * _outDecimals / _inDecimals / 1e18; + return amountIn * _pIn2Out / 1e18;// _outDecimals / _inDecimals } - function _findTokenInBalancePool(address _token, address[] memory _tokens) internal view returns (uint256){ + function _findTokenInBalancePool(address _token, address[] memory _tokens) internal pure returns (uint256){ uint256 _len = _tokens.length; for (uint256 i = 0; i < _len; ){ if (_tokens[i] == _token){ @@ -606,4 +603,10 @@ contract OnChainPricingMainnet { function convertToBytes32(address _input) public pure returns (bytes32){ return bytes32(uint256(uint160(_input)) << 96); } + + /// @dev Take for example the _input "0x111122223333444455556666777788889999AAAABBBBCCCCDDDDEEEEFFFFCCCC" + /// @return the result of "0x111122223333444455556666777788889999aAaa" + function getAddressFromBytes32(bytes32 _input) public pure returns (address){ + return address(uint160(bytes20(_input))); + } } \ No newline at end of file From 865a4348883932b6ab36093454d07baaf5c9c1d1 Mon Sep 17 00:00:00 2001 From: rayeaster Date: Tue, 19 Jul 2022 14:38:39 +0800 Subject: [PATCH 05/53] gas-optimize uniswap v2 quote --- contracts/OnChainPricingMainnet.sol | 78 ++++++++++++++++++++++------- 1 file changed, 61 insertions(+), 17 deletions(-) diff --git a/contracts/OnChainPricingMainnet.sol b/contracts/OnChainPricingMainnet.sol index 1475eb6..1ca29fb 100644 --- a/contracts/OnChainPricingMainnet.sol +++ b/contracts/OnChainPricingMainnet.sol @@ -50,8 +50,10 @@ contract OnChainPricingMainnet { /// == Uni V2 Like Routers || These revert on non-existent pair == // // UniV2 address public constant UNIV2_ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; // Spookyswap + bytes public constant UNIV2_POOL_INITCODE = hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f'; // Sushi address public constant SUSHI_ROUTER = 0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F; + bytes public constant SUSHI_POOL_INITCODE = hex'e18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303'; // Curve / Doesn't revert on failure address public constant CURVE_ROUTER = 0x8e764bE4288B842791989DB5b8ec067279829809; // Curve quote and swaps @@ -232,24 +234,56 @@ contract OnChainPricingMainnet { /// @dev Given the address of the UniV2Like Router, the input amount, and the path, returns the quote for it function getUniPrice(address router, address tokenIn, address tokenOut, uint256 amountIn) public view returns (uint256) { - address[] memory path = new address[](2); - path[0] = address(tokenIn); - path[1] = address(tokenOut); + + // check pool existence first before quote against it + bytes memory _initCode = (router == UNIV2_ROUTER)? UNIV2_POOL_INITCODE : SUSHI_POOL_INITCODE; + (address _pool, address _token0, address _token1) = pairForUniV2(IUniswapRouterV2(router).factory(), tokenIn, tokenOut, _initCode); + if (!_pool.isContract()){ + return 0; + } + + bool _zeroForOne = (_token0 == tokenIn); + // Use LP token Total Supply as a quick-easy substitute for liquidity + (bool _basicCheck, uint256 _t0Balance, uint256 _t1Balance) = _checkPoolLiquidityAndBalances(_pool, IERC20(_pool).totalSupply(), _token0, _token1, _zeroForOne, amountIn); + return _basicCheck? getUniV2AmountOutAnalytically(amountIn, (_zeroForOne? _t0Balance : _t1Balance), (_zeroForOne? _t1Balance : _t0Balance)) : 0; + + //address[] memory path = new address[](2); + //path[0] = address(tokenIn); + //path[1] = address(tokenOut); - uint256 quote; //0 + //uint256 quote; //0 // TODO: Consider doing check before revert to avoid paying extra gas // Specifically, test gas if we get revert vs if we check to avoid it - try IUniswapRouterV2(router).getAmountsOut(amountIn, path) returns (uint256[] memory amounts) { - quote = amounts[amounts.length - 1]; // Last one is the outToken - } catch (bytes memory) { + //try IUniswapRouterV2(router).getAmountsOut(amountIn, path) returns (uint256[] memory amounts) { + // quote = amounts[amounts.length - 1]; // Last one is the outToken + //} catch (bytes memory) { // We ignore as it means it's zero - } + //} - return quote; + //return quote; } - + + /// @dev reference https://etherscan.io/address/0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F#code#L122 + function getUniV2AmountOutAnalytically(uint256 amountIn, uint256 reserveIn, uint256 reserveOut) public pure returns (uint256 amountOut) { + uint256 amountInWithFee = amountIn * 997; + uint256 numerator = amountInWithFee * reserveOut; + uint256 denominator = reserveIn * 1000 + amountInWithFee; + amountOut = numerator / denominator; + } + + function pairForUniV2(address factory, address tokenA, address tokenB, bytes memory _initCode) public view returns (address, address, address) { + (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); + address pair = getAddressFromBytes32Lsb(keccak256(abi.encodePacked( + hex'ff', + factory, + keccak256(abi.encodePacked(token0, token1)), + _initCode // init code hash + ))); + return (pair, token0, token1); + } + /// === UNIV3 === /// /// @dev explore Uniswap V3 pools to check if there is a chance to resolve the swap with in-range liquidity (i.e., without crossing ticks) @@ -303,13 +337,13 @@ contract OnChainPricingMainnet { } } - /// @dev internal function for a basic sanity check Uniswap V3 pool existence and balances + /// @dev internal function for a basic sanity check pool existence and balances /// @return true if basic check pass otherwise false - function _checkUniV3PoolExistenceAndBalances(address _pool, uint128 _liq, address _token0, address _token1, bool token0Price, uint256 amountIn) internal view returns (bool, uint256, uint256) { + function _checkPoolLiquidityAndBalances(address _pool, uint256 _liq, address _token0, address _token1, bool token0Price, uint256 amountIn) internal view returns (bool, uint256, uint256) { { // heuristic check0: ensure the pool [exist] and properly initiated with valid in-range liquidity - if (!_pool.isContract() || _liq == 0) { + if (_liq == 0) { return (false, 0, 0); } } @@ -319,6 +353,7 @@ contract OnChainPricingMainnet { uint256 _t1Balance = IERC20(_token1).balanceOf(_pool); // heuristic check1: ensure the pool tokenIn reserve makes sense in terms of [amountIn], i.e., the pool is liquid compared to swap amount + // _t0Balance and _t1Balance will be both above zero if there is liquidity return ((token0Price? _t0Balance : _t1Balance) > amountIn, _t0Balance, _t1Balance); } } @@ -369,10 +404,13 @@ contract OnChainPricingMainnet { function _getUniV3RateHeuristic(address token0, address token1, uint24 fee, bool token0Price, uint256 amountIn) internal view returns (uint256) { // heuristic check0: ensure the pool [exist] and properly initiated with valid in-range liquidity - address pool = _getUniV3PoolAddress(token0, token1, fee); + address pool = _getUniV3PoolAddress(token0, token1, fee); + if (!pool.isContract()){ + return 0; + } //filter out disqualified pools to save gas on quoter swap query - (bool _basicCheck, uint256 _t0Balance, uint256 _t1Balance) = _checkUniV3PoolExistenceAndBalances(pool, IUniswapV3Pool(pool).liquidity(), token0, token1, token0Price, amountIn); + (bool _basicCheck, uint256 _t0Balance, uint256 _t1Balance) = _checkPoolLiquidityAndBalances(pool, uint256(IUniswapV3Pool(pool).liquidity()), token0, token1, token0Price, amountIn); if (!_basicCheck) return 0; uint256 _t0Decimals = 10 ** IERC20Metadata(token0).decimals(); @@ -463,7 +501,7 @@ contract OnChainPricingMainnet { return 0; } - address _pool = getAddressFromBytes32(poolId); + address _pool = getAddressFromBytes32Msb(poolId); uint256 _pIn2Out; { @@ -606,7 +644,13 @@ contract OnChainPricingMainnet { /// @dev Take for example the _input "0x111122223333444455556666777788889999AAAABBBBCCCCDDDDEEEEFFFFCCCC" /// @return the result of "0x111122223333444455556666777788889999aAaa" - function getAddressFromBytes32(bytes32 _input) public pure returns (address){ + function getAddressFromBytes32Msb(bytes32 _input) public pure returns (address){ return address(uint160(bytes20(_input))); } + + /// @dev Take for example the _input "0x111122223333444455556666777788889999AAAABBBBCCCCDDDDEEEEFFFFCCCC" + /// @return the result of "0x777788889999AaAAbBbbCcccddDdeeeEfFFfCcCc" + function getAddressFromBytes32Lsb(bytes32 _input) public pure returns (address){ + return address(uint160(uint256(_input))); + } } \ No newline at end of file From 4e089b41d3de9e359116e3d71bc3b05c11359202 Mon Sep 17 00:00:00 2001 From: rayeaster Date: Tue, 19 Jul 2022 23:10:15 +0800 Subject: [PATCH 06/53] add tests for Uni V3 simulation against official quoter --- contracts/OnChainPricingMainnet.sol | 4 +- tests/on_chain_pricer/test_univ3_pricer.py | 45 ++++++++++++++++++++-- 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/contracts/OnChainPricingMainnet.sol b/contracts/OnChainPricingMainnet.sol index 1ca29fb..0d194e2 100644 --- a/contracts/OnChainPricingMainnet.sol +++ b/contracts/OnChainPricingMainnet.sol @@ -301,7 +301,7 @@ contract OnChainPricingMainnet { uint256 _outAmt; { // in-range swap check: find out whether the swap within current liquidity would move the price across next tick - (bool _outOfInRange, uint256 _outputAmount) = _checkUniV3InRangeLiquidity(tokenIn, tokenOut, amountIn, _fee, uniV3SimSlippageBps); + (bool _outOfInRange, uint256 _outputAmount) = checkUniV3InRangeLiquidity(tokenIn, tokenOut, amountIn, _fee, uniV3SimSlippageBps); _crossTick = _outOfInRange; _outAmt = _outputAmount; } @@ -325,7 +325,7 @@ contract OnChainPricingMainnet { } /// @dev internal function to avoid stack too deap for Uniswap V3 pool in-range liquidity check - function _checkUniV3InRangeLiquidity(address tokenIn, address tokenOut, uint256 amountIn, uint24 _fee, uint256 _uniV3SimSlippageBps) internal view returns (bool, uint256){ + function checkUniV3InRangeLiquidity(address tokenIn, address tokenOut, uint256 amountIn, uint24 _fee, uint256 _uniV3SimSlippageBps) public view returns (bool, uint256){ (address token0, address token1, bool token0Price) = _ifUniV3Token0Price(tokenIn, tokenOut); { address _pool = _getUniV3PoolAddress(token0, token1, _fee); diff --git a/tests/on_chain_pricer/test_univ3_pricer.py b/tests/on_chain_pricer/test_univ3_pricer.py index 2a588a3..f5f353b 100644 --- a/tests/on_chain_pricer/test_univ3_pricer.py +++ b/tests/on_chain_pricer/test_univ3_pricer.py @@ -5,15 +5,54 @@ """ getUniV3Price quote for token A swapped to token B directly: A - > B """ -def test_get_univ3_price(oneE18, weth, usdc, pricer): +def test_get_univ3_price_in_range(oneE18, weth, usdc, usdc_whale, pricer): ## 1e18 sell_amount = 1 * oneE18 ## minimum quote for ETH in USDC(1e6) ## Rip ETH price p = 1 * 900 * 1000000 - quote = pricer.getUniV3Price(weth.address, sell_amount, usdc.address) + quote = pricer.sortUniV3Pools(weth.address, sell_amount, usdc.address) + print(quote) + assert quote[0] >= p + quoteInRange = pricer.checkUniV3InRangeLiquidity(weth.address, usdc.address, sell_amount, quote[1], 100) + print(quoteInRange) + assert quote[0] == quoteInRange[1] - assert quote >= p + ## check against quoter + quoterP = interface.IV3Quoter(pricer.UNIV3_QUOTER()).quoteExactInputSingle(weth.address, usdc.address, quote[1], sell_amount, 0, {'from': usdc_whale.address}).return_value + print(quoterP) + assert quoterP == quote[0] + + ## fee-0.05% pool is the chosen one among (0.05%, 0.3%, 1%)! + assert quote[1] == 500 + +""" + getUniV3Price quote for token A swapped to token B directly: A - > B +""" +def test_get_univ3_price_cross_tick(oneE18, weth, usdc, usdc_whale, pricer): + ## 1e18 + sell_count = 2000 + sell_amount = sell_count * oneE18 + + ## minimum quote for ETH in USDC(1e6) ## Rip ETH price + p = sell_count * 900 * 1000000 + quote = pricer.sortUniV3Pools(weth.address, sell_amount, usdc.address) + print(quote) + assert quote[0] >= p + quoteCrossTicks = pricer.simulateUniV3Swap(weth.address, sell_amount, usdc.address, quote[1], 100) + print(quoteCrossTicks) + assert quote[0] == quoteCrossTicks + + ## check against quoter + slot0 = interface.IUniswapV3Pool("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640").slot0() + currentPX96 = slot0[0] + limitP = currentPX96 * (100 + 10000) / 10000 + quoterP = interface.IV3Quoter(pricer.UNIV3_QUOTER()).quoteExactInputSingle(weth.address, usdc.address, quote[1], sell_amount, limitP, {'from': usdc_whale.address}).return_value + print(quoterP) + assert (abs(quoterP - quote[0]) / quoterP) <= 0.0003 ## tens of thousandsth in quote error for a millions-dollar-worth swap + + ## fee-0.05% pool is the chosen one among (0.05%, 0.3%, 1%)! + assert quote[1] == 500 """ getUniV3PriceWithConnector quote for token A swapped to token B with connector token C: A -> C -> B From edaae8c7217e81a57d969ca0acfb5e47d24fb98f Mon Sep 17 00:00:00 2001 From: rayeaster Date: Wed, 20 Jul 2022 01:40:23 +0800 Subject: [PATCH 07/53] add pool existence check for quote with connector token --- contracts/OnChainPricingMainnet.sol | 25 +++++++++++++++++++++- tests/on_chain_pricer/test_univ3_pricer.py | 4 ++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/contracts/OnChainPricingMainnet.sol b/contracts/OnChainPricingMainnet.sol index 0d194e2..66e2cae 100644 --- a/contracts/OnChainPricingMainnet.sol +++ b/contracts/OnChainPricingMainnet.sol @@ -324,7 +324,26 @@ contract OnChainPricingMainnet { return (_maxInRangeQuote, _maxInRangeFee); } - /// @dev internal function to avoid stack too deap for Uniswap V3 pool in-range liquidity check + /// @dev tell if there exists some Uniswap V3 pool for given token pair + function checkUniV3PoolsExistence(address tokenIn, address tokenOut) public view returns (bool){ + uint256 feeTypes = univ3_fees.length; + (address token0, address token1, bool token0Price) = _ifUniV3Token0Price(tokenIn, tokenOut); + bool _exist; + { + for (uint256 i = 0; i < feeTypes;){ + address _pool = _getUniV3PoolAddress(token0, token1, univ3_fees[i]); + if (_pool.isContract()) { + _exist = true; + break; + } + unchecked { ++i; } + } + } + return _exist; + } + + /// @dev Uniswap V3 pool in-range liquidity check + /// @return true if cross-ticks full simulation required for the swap otherwise false (in-range liquidity would satisfy the swap) function checkUniV3InRangeLiquidity(address tokenIn, address tokenOut, uint256 amountIn, uint24 _fee, uint256 _uniV3SimSlippageBps) public view returns (bool, uint256){ (address token0, address token1, bool token0Price) = _ifUniV3Token0Price(tokenIn, tokenOut); { @@ -376,6 +395,10 @@ contract OnChainPricingMainnet { /// @dev Given the address of the input token & amount & the output token & connector token in between (input token ---> connector token ---> output token) /// @return the quote for it function getUniV3PriceWithConnector(address tokenIn, uint256 amountIn, address tokenOut, address connectorToken) public view returns (uint256) { + if (!checkUniV3PoolsExistence(tokenIn, connectorToken) || !checkUniV3PoolsExistence(connectorToken, tokenOut)){ + return 0; + } + uint256 connectorAmount = getUniV3Price(tokenIn, amountIn, connectorToken); if (connectorAmount > 0){ return getUniV3Price(connectorToken, connectorAmount, tokenOut); diff --git a/tests/on_chain_pricer/test_univ3_pricer.py b/tests/on_chain_pricer/test_univ3_pricer.py index f5f353b..c052886 100644 --- a/tests/on_chain_pricer/test_univ3_pricer.py +++ b/tests/on_chain_pricer/test_univ3_pricer.py @@ -44,12 +44,12 @@ def test_get_univ3_price_cross_tick(oneE18, weth, usdc, usdc_whale, pricer): assert quote[0] == quoteCrossTicks ## check against quoter - slot0 = interface.IUniswapV3Pool("0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640").slot0() + slot0 = interface.IUniswapV3Pool("0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640").slot0() currentPX96 = slot0[0] limitP = currentPX96 * (100 + 10000) / 10000 quoterP = interface.IV3Quoter(pricer.UNIV3_QUOTER()).quoteExactInputSingle(weth.address, usdc.address, quote[1], sell_amount, limitP, {'from': usdc_whale.address}).return_value print(quoterP) - assert (abs(quoterP - quote[0]) / quoterP) <= 0.0003 ## tens of thousandsth in quote error for a millions-dollar-worth swap + assert (abs(quoterP - quote[0]) / quoterP) <= 0.001 ## thousandsth in quote diff for a millions-dollar-worth swap ## fee-0.05% pool is the chosen one among (0.05%, 0.3%, 1%)! assert quote[1] == 500 From 7d6f70684cd450f340d1a39acbad02797778e10f Mon Sep 17 00:00:00 2001 From: rayeaster Date: Wed, 20 Jul 2022 13:49:40 +0800 Subject: [PATCH 08/53] add single pool choice for Uni V3 mainstream tokens --- contracts/OnChainPricingMainnet.sol | 150 +++++++++++------- contracts/UniV3SwapSimulator.sol | 20 +-- interfaces/uniswap/IV3Simulator.sol | 3 +- tests/on_chain_pricer/test_univ3_pricer.py | 19 +-- .../on_chain_pricer/test_univ3_pricer_simu.py | 4 +- 5 files changed, 110 insertions(+), 86 deletions(-) diff --git a/contracts/OnChainPricingMainnet.sol b/contracts/OnChainPricingMainnet.sol index 66e2cae..07d03a6 100644 --- a/contracts/OnChainPricingMainnet.sol +++ b/contracts/OnChainPricingMainnet.sol @@ -7,7 +7,6 @@ import {IERC20Metadata} from "@oz/token/ERC20/extensions/IERC20Metadata.sol"; import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol"; import {Address} from "@oz/utils/Address.sol"; - import "../interfaces/uniswap/IUniswapRouterV2.sol"; import "../interfaces/uniswap/IV3Pool.sol"; import "../interfaces/uniswap/IV3Quoter.sol"; @@ -109,11 +108,10 @@ contract OnChainPricingMainnet { address public constant AURABAL = 0x616e8BfA43F920657B3497DBf40D6b1A02D4608d; address public constant BALWETHBPT = 0x5c6Ee304399DBdB9C8Ef030aB642B10820DB8F56; uint256 public constant CURVE_FEE_SCALE = 100000; + address public constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; /// @dev helper library to simulate Uniswap V3 swap address public uniV3Simulator; - /// @dev slippage allowance bps in Uniswap V3 simulation, max 10000 - uint256 public uniV3SimSlippageBps = 100; // 1% slippage allowed in simulation /// === TEST-ONLY === constructor(address _uniV3Simulator){ @@ -124,11 +122,6 @@ contract OnChainPricingMainnet { require(_uniV3Simulator != address(0));//TODO permission uniV3Simulator = _uniV3Simulator; } - - function setUniV3SimuSlippageBps(uint256 _slippageBps) external { - require(_slippageBps < 10000);//TODO permission - uniV3SimSlippageBps = _slippageBps; - } /// === END TEST-ONLY === struct Quote { @@ -151,7 +144,7 @@ contract OnChainPricingMainnet { } // If no pool this is fairly cheap, else highly likely there's a price - if(getUniV3Price(tokenIn, amountIn, tokenOut) > 0) { + if(checkUniV3PoolsExistence(tokenIn, tokenOut)) { return true; } @@ -295,29 +288,24 @@ contract OnChainPricingMainnet { uint24 _maxInRangeFee; { + uint24 _bestFee = _useSinglePoolInUniV3(tokenIn, tokenOut); for (uint256 i = 0; i < feeTypes;){ uint24 _fee = univ3_fees[i]; - bool _crossTick; - uint256 _outAmt; - { - // in-range swap check: find out whether the swap within current liquidity would move the price across next tick - (bool _outOfInRange, uint256 _outputAmount) = checkUniV3InRangeLiquidity(tokenIn, tokenOut, amountIn, _fee, uniV3SimSlippageBps); - _crossTick = _outOfInRange; - _outAmt = _outputAmount; - } - { - // unfortunately we need to do a full simulation to cross ticks - if (_crossTick){ - _outAmt = simulateUniV3Swap(tokenIn, amountIn, tokenOut, _fee, uniV3SimSlippageBps); - } + + // skip othter pools if there is a chosen best pool to go + if (_bestFee > 0 && _fee != _bestFee){ + unchecked { ++i; } + continue; } + { + (bool _crossTick, uint256 _outAmt) = _checkSimulationInUniV3(tokenIn, tokenOut, amountIn, _fee); if (_outAmt > _maxInRangeQuote){ _maxInRangeQuote = _outAmt; _maxInRangeFee = _fee; } + unchecked { ++i; } } - unchecked { ++i; } } } @@ -344,18 +332,37 @@ contract OnChainPricingMainnet { /// @dev Uniswap V3 pool in-range liquidity check /// @return true if cross-ticks full simulation required for the swap otherwise false (in-range liquidity would satisfy the swap) - function checkUniV3InRangeLiquidity(address tokenIn, address tokenOut, uint256 amountIn, uint24 _fee, uint256 _uniV3SimSlippageBps) public view returns (bool, uint256){ + function checkUniV3InRangeLiquidity(address tokenIn, address tokenOut, uint256 amountIn, uint24 _fee) public view returns (bool, uint256){ (address token0, address token1, bool token0Price) = _ifUniV3Token0Price(tokenIn, tokenOut); { address _pool = _getUniV3PoolAddress(token0, token1, _fee); if (!_pool.isContract() || IUniswapV3Pool(_pool).liquidity() <= 0) { return (false, 0); } - UniV3SortPoolQuery memory _sortQuery = UniV3SortPoolQuery(_pool, token0, token1, _fee, amountIn, token0Price, _uniV3SimSlippageBps); + UniV3SortPoolQuery memory _sortQuery = UniV3SortPoolQuery(_pool, token0, token1, _fee, amountIn, token0Price); return IUniswapV3Simulator(uniV3Simulator).checkInRangeLiquidity(_sortQuery); } } + /// @dev internal function to avoid stack too deap for 1) check in-range liquidity in Uniswap V3 pool 2) full cross-ticks simulation in Uniswap V3 + function _checkSimulationInUniV3(address tokenIn, address tokenOut, uint256 amountIn, uint24 _fee) internal view returns (bool, uint256) { + bool _crossTick; + uint256 _outAmt; + { + // in-range swap check: find out whether the swap within current liquidity would move the price across next tick + (bool _outOfInRange, uint256 _outputAmount) = checkUniV3InRangeLiquidity(tokenIn, tokenOut, amountIn, _fee); + _crossTick = _outOfInRange; + _outAmt = _outputAmount; + } + { + // unfortunately we need to do a full simulation to cross ticks + if (_crossTick){ + _outAmt = simulateUniV3Swap(tokenIn, amountIn, tokenOut, _fee); + } + } + return (_crossTick, _outAmt); + } + /// @dev internal function for a basic sanity check pool existence and balances /// @return true if basic check pass otherwise false function _checkPoolLiquidityAndBalances(address _pool, uint256 _liq, address _token0, address _token1, bool token0Price, uint256 amountIn) internal view returns (bool, uint256, uint256) { @@ -379,10 +386,10 @@ contract OnChainPricingMainnet { /// @dev simulate Uniswap V3 swap using its tick-based math for given parameters /// @dev check helper UniV3SwapSimulator for more - function simulateUniV3Swap(address tokenIn, uint256 amountIn, address tokenOut, uint24 _fee, uint256 _slippageAllowedBps) public view returns (uint256){ + function simulateUniV3Swap(address tokenIn, uint256 amountIn, address tokenOut, uint24 _fee) public view returns (uint256){ (address token0, address token1, bool token0Price) = _ifUniV3Token0Price(tokenIn, tokenOut); address _pool = _getUniV3PoolAddress(token0, token1, _fee); - return IUniswapV3Simulator(uniV3Simulator).simulateUniV3Swap(_pool, token0, token1, token0Price, _fee, amountIn, _slippageAllowedBps); + return IUniswapV3Simulator(uniV3Simulator).simulateUniV3Swap(_pool, token0, token1, token0Price, _fee, amountIn); } /// @dev Given the address of the input token & amount & the output token @@ -395,7 +402,8 @@ contract OnChainPricingMainnet { /// @dev Given the address of the input token & amount & the output token & connector token in between (input token ---> connector token ---> output token) /// @return the quote for it function getUniV3PriceWithConnector(address tokenIn, uint256 amountIn, address tokenOut, address connectorToken) public view returns (uint256) { - if (!checkUniV3PoolsExistence(tokenIn, connectorToken) || !checkUniV3PoolsExistence(connectorToken, tokenOut)){ + // Skip if there is a mainstrem direct swap or connector pools not exist + if (_useSinglePoolInUniV3(tokenIn, tokenOut) > 0 || !checkUniV3PoolsExistence(tokenIn, connectorToken) || !checkUniV3PoolsExistence(connectorToken, tokenOut)){ return 0; } @@ -424,37 +432,27 @@ contract OnChainPricingMainnet { /// @dev with trade amount & indicator if token0 pricing required (token1/token0 e.g., token0 -> token1) /// @dev note there are some heuristic checks around the price like pool reserve should satisfy the swap amount /// @return the current price in V3 for it - function _getUniV3RateHeuristic(address token0, address token1, uint24 fee, bool token0Price, uint256 amountIn) internal view returns (uint256) { - - // heuristic check0: ensure the pool [exist] and properly initiated with valid in-range liquidity - address pool = _getUniV3PoolAddress(token0, token1, fee); - if (!pool.isContract()){ - return 0; - } - - //filter out disqualified pools to save gas on quoter swap query - (bool _basicCheck, uint256 _t0Balance, uint256 _t1Balance) = _checkPoolLiquidityAndBalances(pool, uint256(IUniswapV3Pool(pool).liquidity()), token0, token1, token0Price, amountIn); - if (!_basicCheck) return 0; - - uint256 _t0Decimals = 10 ** IERC20Metadata(token0).decimals(); - uint256 _t1Decimals = 10 ** IERC20Metadata(token1).decimals(); + //function _getUniV3RateHeuristic(address token0, address token1, uint24 fee, bool token0Price, uint256 amountIn) internal view returns (uint256) { + + //uint256 _t0Decimals = 10 ** IERC20Metadata(token0).decimals(); + //uint256 _t1Decimals = 10 ** IERC20Metadata(token1).decimals(); // Heuristically reserve with spot price check: ensure the pool tokenOut reserve makes sense in terms of thespot price [amountOutput based on slot0 price] - (uint256 rate, uint256 amountOutput) = _getOutputWithSlot0Price(token0, token1, pool, token0Price, _t0Decimals, _t1Decimals, amountIn); - if ((token0Price? _t1Balance : _t0Balance) <= amountOutput){ - return 0; - } + //(uint256 rate, uint256 amountOutput) = _getOutputWithSlot0Price(token0, token1, pool, token0Price, _t0Decimals, _t1Decimals, amountIn); + //if ((token0Price? _t1Balance : _t0Balance) <= amountOutput){ + // return 0; + //} // Heuristically reserves comparison check: ensure the pool [reserve comparison is consistent with the slot0 price comparison], // i.e., asset in less amount should be more expensive in AMM pool - bool token0MoreExpensive = _compareUniV3Tokens(token0Price, rate); - bool token0MoreReserved = _compareUniV3TokenReserves(_t0Balance, _t1Balance, _t0Decimals, _t1Decimals); - if (token0MoreExpensive == token0MoreReserved){ - return 0; - } + //bool token0MoreExpensive = _compareUniV3Tokens(token0Price, rate); + //bool token0MoreReserved = _compareUniV3TokenReserves(_t0Balance, _t1Balance, _t0Decimals, _t1Decimals); + //if (token0MoreExpensive == token0MoreReserved){ + // return 0; + //} - return amountOutput; - } + //return amountOutput; + //} /// @dev calculate output amount according to Uniswap V3 spot price (slot0) function _getOutputWithSlot0Price(address token0, address token1, address pool, bool token0Price, uint256 _t0Decimals, uint256 _t1Decimals, uint256 amountIn) internal view returns (uint256, uint256) { @@ -476,14 +474,14 @@ contract OnChainPricingMainnet { } /// @dev check if token0 is more expensive than token1 given slot0 price & if token0 pricing required - function _compareUniV3Tokens(bool token0Price, uint256 rate) internal view returns (bool) { - return token0Price? (rate > 1e18) : (rate < 1e18); - } + //function _compareUniV3Tokens(bool token0Price, uint256 rate) internal view returns (bool) { + // return token0Price? (rate > 1e18) : (rate < 1e18); + //} /// @dev check if token0 reserve is bigger than token1 reserve - function _compareUniV3TokenReserves(uint256 _t0Balance, uint256 _t1Balance, uint256 _t0Decimals, uint256 _t1Decimals) internal view returns (bool) { - return (_t0Balance / _t0Decimals) > (_t1Balance / _t1Decimals); - } + //function _compareUniV3TokenReserves(uint256 _t0Balance, uint256 _t1Balance, uint256 _t0Decimals, uint256 _t1Decimals) internal view returns (bool) { + // return (_t0Balance / _t0Decimals) > (_t1Balance / _t1Decimals); + //} /// @dev query with the address of the token0 & token1 & the fee tier /// @return the uniswap v3 pool address @@ -491,6 +489,36 @@ contract OnChainPricingMainnet { bytes32 addr = keccak256(abi.encodePacked(hex"ff", UNIV3_FACTORY, keccak256(abi.encode(token0, token1, fee)), UNIV3_POOL_INIT_CODE_HASH)); return address(uint160(uint256(addr))); } + + /// @dev selected token pair which will try a chosen Uniswap V3 pool ONLY among all possible fees + /// @dev picked from most traded pool (Volume 7D) in https://info.uniswap.org/#/pools + /// @dev mainly 5 most-popular tokens WETH-WBTC-USDC-USDT-DAI (Volume 24H) https://info.uniswap.org/#/tokens + /// @return 0 if all possible fees should be checked otherwise the ONLY pool fee we should go for + function _useSinglePoolInUniV3(address tokenIn, address tokenOut) internal pure returns(uint24) { + if ((tokenIn == WETH && tokenOut == USDC) || (tokenOut == USDC && tokenIn == WETH)){ + return 500; + } else if ((tokenIn == WETH && tokenOut == WBTC) || (tokenOut == WBTC && tokenIn == WETH)){ + return 500; + } else if ((tokenIn == WETH && tokenOut == USDT) || (tokenOut == USDT && tokenIn == WETH)){ + return 500; + } else if ((tokenIn == WETH && tokenOut == DAI) || (tokenOut == DAI && tokenIn == WETH)){ + return 500; + } else if ((tokenIn == USDC && tokenOut == USDT) || (tokenOut == USDT && tokenIn == USDC)){ + return 100; + } else if ((tokenIn == USDC && tokenOut == DAI) || (tokenOut == DAI && tokenIn == USDC)){ + return 100; + } else if ((tokenIn == USDC && tokenOut == WBTC) || (tokenOut == WBTC && tokenIn == USDC)){ + return 3000; + } else if ((tokenIn == WBTC && tokenOut == USDT) || (tokenOut == USDT && tokenIn == WBTC)){ + return 0;// TVL too small + } else if ((tokenIn == WBTC && tokenOut == DAI) || (tokenOut == DAI && tokenIn == WBTC)){ + return 0;// TVL too small + } else if ((tokenIn == DAI && tokenOut == USDT) || (tokenOut == USDT && tokenIn == DAI)){ + return 0;// TVL too small + } else { + return 0; + } + } /// === BALANCER === /// @@ -585,6 +613,10 @@ contract OnChainPricingMainnet { /// @dev Given the input/output/connector token, returns the quote for input amount from Balancer V2 using its underlying math function getBalancerPriceWithConnectorAnalytically(address tokenIn, uint256 amountIn, address tokenOut, address connectorToken) public view returns (uint256) { + if (getBalancerV2Pool(tokenIn, connectorToken) == BALANCERV2_NONEXIST_POOLID || getBalancerV2Pool(connectorToken, tokenOut) == BALANCERV2_NONEXIST_POOLID){ + return 0; + } + uint256 _in2ConnectorAmt = getBalancerPriceAnalytically(tokenIn, amountIn, connectorToken); if (_in2ConnectorAmt <= 0){ return 0; diff --git a/contracts/UniV3SwapSimulator.sol b/contracts/UniV3SwapSimulator.sol index 2571528..8e1f5df 100644 --- a/contracts/UniV3SwapSimulator.sol +++ b/contracts/UniV3SwapSimulator.sol @@ -16,7 +16,6 @@ struct UniV3SortPoolQuery{ uint24 _fee; uint256 amountIn; bool zeroForOne; - uint256 _slippageAllowedBps; } interface IERC20 { @@ -52,10 +51,7 @@ contract UniV3SwapSimulator { /// @dev estimate the expected output for given swap parameters and slippage /// @dev simplified version of https://github.com/Uniswap/v3-core/blob/main/contracts/UniswapV3Pool.sol#L596 /// @return simulated output token amount using Uniswap V3 tick-based math - function simulateUniV3Swap(address _pool, address _token0, address _token1, bool _zeroForOne, uint24 _fee, uint256 _amountIn, uint256 _slippageAllowedBps) external view returns (uint256){ - require(_slippageAllowedBps < MAX_BPS, '!SLIP'); - require(_slippageAllowedBps > 0, '!SLI0'); - + function simulateUniV3Swap(address _pool, address _token0, address _token1, bool _zeroForOne, uint24 _fee, uint256 _amountIn) external view returns (uint256){ // Get current state of the pool int24 _tickSpacing = IUniswapV3PoolSwapTick(_pool).tickSpacing(); // lower limit if zeroForOne in terms of slippage, or upper limit for the other direction @@ -65,7 +61,7 @@ contract UniV3SwapSimulator { { (uint160 _currentPX96, int24 _currentTick,,,,,) = IUniswapV3PoolSwapTick(_pool).slot0(); - _sqrtPriceLimitX96 = _getLimitPriceFromSlippageAllowance(_zeroForOne, _currentPX96, _slippageAllowedBps); + _sqrtPriceLimitX96 = _getLimitPrice(_zeroForOne); state = SwapStatus(_amountIn.toInt256(), _currentPX96, _currentTick, IUniswapV3PoolSwapTick(_pool).liquidity(), 0); } @@ -103,6 +99,11 @@ contract UniV3SwapSimulator { /// @dev retrieve next initialized tick for given Uniswap V3 pool function _getNextInitializedTick(TickNextWithWordQuery memory _nextTickQuery) internal view returns (int24, bool, uint160) { (int24 tickNext, bool initialized) = TickBitmap.nextInitializedTickWithinOneWord(_nextTickQuery); + if (tickNext < TickMath.MIN_TICK) { + tickNext = TickMath.MIN_TICK; + } else if (tickNext > TickMath.MAX_TICK) { + tickNext = TickMath.MAX_TICK; + } uint160 sqrtPriceNextX96 = TickMath.getSqrtRatioAtTick(tickNext); return (tickNext, initialized, sqrtPriceNextX96); } @@ -155,7 +156,7 @@ contract UniV3SwapSimulator { } { - uint160 _targetPX96 = _getTargetPriceForSwapStep(_sortQuery.zeroForOne, _tickNextPrice, _getLimitPriceFromSlippageAllowance(_sortQuery.zeroForOne, _currentPriceX96, _sortQuery._slippageAllowedBps)); + uint160 _targetPX96 = _getTargetPriceForSwapStep(_sortQuery.zeroForOne, _tickNextPrice, _getLimitPrice(_sortQuery.zeroForOne)); SwapExactInParam memory _exactInParams = SwapExactInParam(_sortQuery.amountIn, _sortQuery._fee, _currentPriceX96, _targetPX96, _liq, _sortQuery.zeroForOne); (uint256 _amtIn, uint160 _newPrice) = SwapMath._getExactInNextPrice(_exactInParams); _swapAfterPrice = _newPrice; @@ -164,8 +165,9 @@ contract UniV3SwapSimulator { return (_swapAfterPrice, _tickNextPrice, _currentPriceX96); } - function _getLimitPriceFromSlippageAllowance(bool _zeroForOne, uint160 _currentPX96, uint256 _slippageAllowedBps) internal pure returns (uint160) { - return _zeroForOne? (_currentPX96 * (MAX_BPS - _slippageAllowedBps).toUint160() / uint160(MAX_BPS)) : (_currentPX96 * (MAX_BPS + _slippageAllowedBps).toUint160() / uint160(MAX_BPS)); + /// @dev https://etherscan.io/address/0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6#code#F1#L95 + function _getLimitPrice(bool _zeroForOne) internal pure returns (uint160) { + return _zeroForOne? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1; } function _getTargetPriceForSwapStep(bool _zeroForOne, uint160 sqrtPriceNextX96, uint160 _sqrtPriceLimitX96) internal pure returns (uint160) { diff --git a/interfaces/uniswap/IV3Simulator.sol b/interfaces/uniswap/IV3Simulator.sol index a0ba47c..5f5c6d8 100644 --- a/interfaces/uniswap/IV3Simulator.sol +++ b/interfaces/uniswap/IV3Simulator.sol @@ -17,10 +17,9 @@ struct UniV3SortPoolQuery{ uint24 _fee; uint256 amountIn; bool zeroForOne; - uint256 _slippageAllowedBps; } interface IUniswapV3Simulator { - function simulateUniV3Swap(address _pool, address _token0, address _token1, bool _zeroForOne, uint24 _fee, uint256 _amountIn, uint256 _slippageAllowedBps) external view returns (uint256); + function simulateUniV3Swap(address _pool, address _token0, address _token1, bool _zeroForOne, uint24 _fee, uint256 _amountIn) external view returns (uint256); function checkInRangeLiquidity(UniV3SortPoolQuery memory _sortQuery) external view returns (bool, uint256); } \ No newline at end of file diff --git a/tests/on_chain_pricer/test_univ3_pricer.py b/tests/on_chain_pricer/test_univ3_pricer.py index c052886..c86fd4f 100644 --- a/tests/on_chain_pricer/test_univ3_pricer.py +++ b/tests/on_chain_pricer/test_univ3_pricer.py @@ -7,20 +7,17 @@ """ def test_get_univ3_price_in_range(oneE18, weth, usdc, usdc_whale, pricer): ## 1e18 - sell_amount = 1 * oneE18 + sell_amount = 20 * oneE18 ## minimum quote for ETH in USDC(1e6) ## Rip ETH price p = 1 * 900 * 1000000 quote = pricer.sortUniV3Pools(weth.address, sell_amount, usdc.address) - print(quote) assert quote[0] >= p - quoteInRange = pricer.checkUniV3InRangeLiquidity(weth.address, usdc.address, sell_amount, quote[1], 100) - print(quoteInRange) + quoteInRange = pricer.checkUniV3InRangeLiquidity(weth.address, usdc.address, sell_amount, quote[1]) assert quote[0] == quoteInRange[1] ## check against quoter quoterP = interface.IV3Quoter(pricer.UNIV3_QUOTER()).quoteExactInputSingle(weth.address, usdc.address, quote[1], sell_amount, 0, {'from': usdc_whale.address}).return_value - print(quoterP) assert quoterP == quote[0] ## fee-0.05% pool is the chosen one among (0.05%, 0.3%, 1%)! @@ -37,19 +34,13 @@ def test_get_univ3_price_cross_tick(oneE18, weth, usdc, usdc_whale, pricer): ## minimum quote for ETH in USDC(1e6) ## Rip ETH price p = sell_count * 900 * 1000000 quote = pricer.sortUniV3Pools(weth.address, sell_amount, usdc.address) - print(quote) assert quote[0] >= p - quoteCrossTicks = pricer.simulateUniV3Swap(weth.address, sell_amount, usdc.address, quote[1], 100) - print(quoteCrossTicks) + quoteCrossTicks = pricer.simulateUniV3Swap(weth.address, sell_amount, usdc.address, quote[1]) assert quote[0] == quoteCrossTicks ## check against quoter - slot0 = interface.IUniswapV3Pool("0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640").slot0() - currentPX96 = slot0[0] - limitP = currentPX96 * (100 + 10000) / 10000 - quoterP = interface.IV3Quoter(pricer.UNIV3_QUOTER()).quoteExactInputSingle(weth.address, usdc.address, quote[1], sell_amount, limitP, {'from': usdc_whale.address}).return_value - print(quoterP) - assert (abs(quoterP - quote[0]) / quoterP) <= 0.001 ## thousandsth in quote diff for a millions-dollar-worth swap + quoterP = interface.IV3Quoter(pricer.UNIV3_QUOTER()).quoteExactInputSingle(weth.address, usdc.address, quote[1], sell_amount, 0, {'from': usdc_whale.address}).return_value + assert (abs(quoterP - quote[0]) / quoterP) <= 0.0015 ## thousandsth in quote diff for a millions-dollar-worth swap ## fee-0.05% pool is the chosen one among (0.05%, 0.3%, 1%)! assert quote[1] == 500 diff --git a/tests/on_chain_pricer/test_univ3_pricer_simu.py b/tests/on_chain_pricer/test_univ3_pricer_simu.py index 0560f83..a1d833e 100644 --- a/tests/on_chain_pricer/test_univ3_pricer_simu.py +++ b/tests/on_chain_pricer/test_univ3_pricer_simu.py @@ -12,7 +12,7 @@ def test_simu_univ3_swap(oneE18, weth, usdc, pricer): ## minimum quote for ETH in USDC(1e6) ## Rip ETH price p = sell_count * 900 * 1000000 - quote = pricer.simulateUniV3Swap(weth.address, sell_amount, usdc.address, 500, 100) + quote = pricer.simulateUniV3Swap(weth.address, sell_amount, usdc.address, 500) assert quote >= p @@ -26,7 +26,7 @@ def test_simu_univ3_swap2(oneE18, weth, wbtc, pricer): ## minimum quote for BTC in ETH(1e18) ## Rip ETH price p = sell_count * 14 * oneE18 - quote = pricer.simulateUniV3Swap(wbtc.address, sell_amount, weth.address, 500, 100) + quote = pricer.simulateUniV3Swap(wbtc.address, sell_amount, weth.address, 500) assert quote >= p From 6013841c4b2427d9fdbb2e389376d78b2a4956c3 Mon Sep 17 00:00:00 2001 From: rayeaster Date: Wed, 20 Jul 2022 19:47:11 +0800 Subject: [PATCH 09/53] add benchmark test for price gas consumption --- contracts/OnChainPricingMainnet.sol | 8 +- tests/gas_benchmark/benchmark_pricer_gas.py | 87 +++++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 tests/gas_benchmark/benchmark_pricer_gas.py diff --git a/contracts/OnChainPricingMainnet.sol b/contracts/OnChainPricingMainnet.sol index 07d03a6..1ed8441 100644 --- a/contracts/OnChainPricingMainnet.sol +++ b/contracts/OnChainPricingMainnet.sol @@ -336,9 +336,15 @@ contract OnChainPricingMainnet { (address token0, address token1, bool token0Price) = _ifUniV3Token0Price(tokenIn, tokenOut); { address _pool = _getUniV3PoolAddress(token0, token1, _fee); - if (!_pool.isContract() || IUniswapV3Pool(_pool).liquidity() <= 0) { + if (!_pool.isContract()) { return (false, 0); } + + (bool _basicCheck,,) = _checkPoolLiquidityAndBalances(_pool, IUniswapV3Pool(_pool).liquidity(), token0, token1, token0Price, amountIn); + if (!_basicCheck) { + return (false, 0); + } + UniV3SortPoolQuery memory _sortQuery = UniV3SortPoolQuery(_pool, token0, token1, _fee, amountIn, token0Price); return IUniswapV3Simulator(uniV3Simulator).checkInRangeLiquidity(_sortQuery); } diff --git a/tests/gas_benchmark/benchmark_pricer_gas.py b/tests/gas_benchmark/benchmark_pricer_gas.py new file mode 100644 index 0000000..85ff6c0 --- /dev/null +++ b/tests/gas_benchmark/benchmark_pricer_gas.py @@ -0,0 +1,87 @@ +import brownie +from brownie import * +import pytest + +""" + Benchmark test for gas cost in findOptimalSwap on various conditions + This file is ok to be exclcuded in test suite due to its underluying functionality should be covered by other tests + Rename the file to test_benchmark_pricer_gas.py to make this part of the testing suite if required +""" + +def test_gas_only_uniswap_v2(oneE18, weth, pricer): + token = "0xBC7250C8c3eCA1DfC1728620aF835FCa489bFdf3" # some swap (GM-WETH) only in Uniswap V2 + ## 1e18 + sell_count = 100000000; + sell_amount = sell_count * 1000000000 ## 1e9 + + tx = pricer.findOptimalSwap(token, weth.address, sell_amount) + assert tx.return_value[0] == 1 ## UNIV2 + assert tx.return_value[1] > 0 + assert tx.gas_used <= 80000 ## 73925 in test simulation + +def test_gas_uniswap_v2_sushi(oneE18, weth, pricer): + token = "0x2e9d63788249371f1DFC918a52f8d799F4a38C94" # some swap (TOKE-WETH) only in Uniswap V2 & SushiSwap + ## 1e18 + sell_count = 5000; + sell_amount = sell_count * oneE18 ## 1e18 + + tx = pricer.findOptimalSwap(token, weth.address, sell_amount) + assert (tx.return_value[0] == 1 or tx.return_value[0] == 2) ## UNIV2 or SUSHI + assert tx.return_value[1] > 0 + assert tx.gas_used <= 90000 ## 83158 in test simulation + +def test_gas_only_balancer_v2(oneE18, weth, aura, pricer): + token = aura # some swap (AURA-WETH) only in Balancer V2 + ## 1e18 + sell_count = 2000; + sell_amount = sell_count * oneE18 ## 1e18 + + tx = pricer.findOptimalSwap(token, weth.address, sell_amount) + assert tx.return_value[0] == 5 ## BALANCER + assert tx.return_value[1] > 0 + assert tx.gas_used <= 90000 ## 87843 in test simulation + +def test_gas_only_balancer_v2_with_weth(oneE18, wbtc, aura, pricer): + token = aura # some swap (AURA-WETH-WBTC) only in Balancer V2 via WETH in between as connector + ## 1e18 + sell_count = 2000; + sell_amount = sell_count * oneE18 ## 1e18 + + tx = pricer.findOptimalSwap(token, wbtc.address, sell_amount) + assert tx.return_value[0] == 6 ## BALANCERWITHWETH + assert tx.return_value[1] > 0 + assert tx.gas_used <= 140000 ## 134538 in test simulation + +def test_gas_only_uniswap_v3(oneE18, weth, pricer): + token = "0xf4d2888d29D722226FafA5d9B24F9164c092421E" # some swap (LOOKS-WETH) only in Uniswap V3 + ## 1e18 + sell_count = 600000; + sell_amount = sell_count * oneE18 ## 1e18 + + tx = pricer.findOptimalSwap(token, weth.address, sell_amount) + assert tx.return_value[0] == 3 ## UNIV3 + assert tx.return_value[1] > 0 + assert tx.gas_used <= 130000 ## 128409 in test simulation + +def test_gas_only_uniswap_v3_with_weth(oneE18, wbtc, pricer): + token = "0xf4d2888d29D722226FafA5d9B24F9164c092421E" # some swap (LOOKS-WETH-WBTC) only in Uniswap V3 via WETH in between as connector + ## 1e18 + sell_count = 600000; + sell_amount = sell_count * oneE18 ## 1e18 + + tx = pricer.findOptimalSwap(token, wbtc.address, sell_amount) + assert tx.return_value[0] == 4 ## UNIV3WITHWETH + assert tx.return_value[1] > 0 + assert tx.gas_used <= 210000 ## 203586 in test simulation + +def test_gas_almost_everything(oneE18, wbtc, weth, pricer): + token = weth # some swap (WETH-WBTC) almost in every DEX, the most gas-consuming scenario + ## 1e18 + sell_count = 10; + sell_amount = sell_count * oneE18 ## 1e18 + + tx = pricer.findOptimalSwap(token, wbtc.address, sell_amount) + assert (tx.return_value[0] <= 3 or tx.return_value[0] == 5) ## CURVE or UNIV2 or SUSHI or UNIV3 or BALANCER + assert tx.return_value[1] > 0 + assert tx.gas_used <= 190000 ## 183810 in test simulation + \ No newline at end of file From 61f54c0395820b08177cf4fbee1d3784462fec49 Mon Sep 17 00:00:00 2001 From: rayeaster Date: Thu, 21 Jul 2022 00:36:22 +0800 Subject: [PATCH 10/53] port balancer weighted exactin swap math --- contracts/BalancerSwapSimulator.sol | 39 ++ contracts/OnChainPricingMainnet.sol | 19 +- contracts/OnChainPricingMainnetLenient.sol | 2 +- contracts/UniV3SwapSimulator.sol | 2 - .../libraries/balancer/BalancerFixedPoint.sol | 107 ++++ .../libraries/balancer/BalancerLogExpMath.sol | 490 ++++++++++++++++++ interfaces/balancer/IBalancerV2Simulator.sol | 15 + tests/conftest.py | 6 +- tests/gas_benchmark/benchmark_pricer_gas.py | 4 +- tests/on_chain_pricer/test_balancer_pricer.py | 4 +- 10 files changed, 674 insertions(+), 14 deletions(-) create mode 100644 contracts/BalancerSwapSimulator.sol create mode 100644 contracts/libraries/balancer/BalancerFixedPoint.sol create mode 100644 contracts/libraries/balancer/BalancerLogExpMath.sol create mode 100644 interfaces/balancer/IBalancerV2Simulator.sol diff --git a/contracts/BalancerSwapSimulator.sol b/contracts/BalancerSwapSimulator.sol new file mode 100644 index 0000000..845f2e9 --- /dev/null +++ b/contracts/BalancerSwapSimulator.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.7.6; +pragma abicoder v2; + +import "./libraries/balancer/BalancerFixedPoint.sol"; + +struct ExactInQueryParam{ + uint256 balanceIn; + uint256 weightIn; + uint256 balanceOut; + uint256 weightOut; + uint256 amountIn; +} + +/// @dev Swap Simulator for Balancer V2 +contract BalancerSwapSimulator { + uint256 internal constant _MAX_IN_RATIO = 0.3e18; + + /// @dev reference https://github.com/balancer-labs/balancer-v2-monorepo/blob/master/pkg/pool-weighted/contracts/WeightedMath.sol#L78 + function calcOutGivenIn(ExactInQueryParam memory _query) public pure returns (uint256) { + /********************************************************************************************** + // outGivenIn // + // aO = amountOut // + // bO = balanceOut // + // bI = balanceIn / / bI \ (wI / wO) \ // + // aI = amountIn aO = bO * | 1 - | -------------------------- | ^ | // + // wI = weightIn \ \ ( bI + aI ) / / // + // wO = weightOut // + **********************************************************************************************/ + require(_query.amountIn <= BalancerFixedPoint.mulDown(_query.balanceIn, _MAX_IN_RATIO), '!maxIn'); + uint256 denominator = BalancerFixedPoint.add(_query.balanceIn, _query.amountIn); + uint256 base = BalancerFixedPoint.divUp(_query.balanceIn, denominator); + uint256 exponent = BalancerFixedPoint.divDown(_query.weightIn, _query.weightOut); + uint256 power = BalancerFixedPoint.powUp(base, exponent); + + return BalancerFixedPoint.mulDown(_query.balanceOut, BalancerFixedPoint.complement(power)); + } + +} \ No newline at end of file diff --git a/contracts/OnChainPricingMainnet.sol b/contracts/OnChainPricingMainnet.sol index 1ed8441..1f32208 100644 --- a/contracts/OnChainPricingMainnet.sol +++ b/contracts/OnChainPricingMainnet.sol @@ -15,6 +15,7 @@ import "../interfaces/balancer/IBalancerV2WeightedPool.sol"; import "../interfaces/curve/ICurveRouter.sol"; import "../interfaces/curve/ICurvePool.sol"; import "../interfaces/uniswap/IV3Simulator.sol"; +import "../interfaces/balancer/IBalancerV2Simulator.sol"; enum SwapType { CURVE, //0 @@ -112,16 +113,24 @@ contract OnChainPricingMainnet { /// @dev helper library to simulate Uniswap V3 swap address public uniV3Simulator; + /// @dev helper library to simulate Balancer V2 swap + address public balancerV2Simulator; /// === TEST-ONLY === - constructor(address _uniV3Simulator){ + constructor(address _uniV3Simulator, address _balancerV2Simulator){ uniV3Simulator = _uniV3Simulator; + balancerV2Simulator = _balancerV2Simulator; } function setUniV3Simulator(address _uniV3Simulator) external { require(_uniV3Simulator != address(0));//TODO permission uniV3Simulator = _uniV3Simulator; } + + function setBalancerV2Simulator(address _balancerV2Simulator) external { + require(_balancerV2Simulator != address(0));//TODO permission + balancerV2Simulator = _balancerV2Simulator; + } /// === END TEST-ONLY === struct Quote { @@ -551,7 +560,6 @@ contract OnChainPricingMainnet { } /// @dev Given the input/output token, returns the quote for input amount from Balancer V2 using its underlying math - /// @dev reference: https://hackmd.io/@shuklaayush/BkAtKbCY9 function getBalancerPriceAnalytically(address tokenIn, uint256 amountIn, address tokenOut) public view returns (uint256) { bytes32 poolId = getBalancerV2Pool(tokenIn, tokenOut); if (poolId == BALANCERV2_NONEXIST_POOLID){ @@ -559,7 +567,7 @@ contract OnChainPricingMainnet { } address _pool = getAddressFromBytes32Msb(poolId); - uint256 _pIn2Out; + uint256 _quote; { uint256[] memory _weights = IBalancerV2WeightedPool(_pool).getNormalizedWeights(); @@ -572,10 +580,11 @@ contract OnChainPricingMainnet { require(_outTokenIdx < tokens.length, '!outBAL'); /// Balancer math for spot price of tokenIn -> tokenOut: weighted value(number * price) relation should be kept - _pIn2Out = 1e18 * (_weights[_inTokenIdx] * balances[_outTokenIdx]) / (_weights[_outTokenIdx] * balances[_inTokenIdx]); // _outDecimals / _inDecimals + ExactInQueryParam memory _query = ExactInQueryParam(balances[_inTokenIdx], _weights[_inTokenIdx], balances[_outTokenIdx], _weights[_outTokenIdx], amountIn); + _quote = IBalancerV2Simulator(balancerV2Simulator).calcOutGivenIn(_query); } - return amountIn * _pIn2Out / 1e18;// _outDecimals / _inDecimals + return _quote; } function _findTokenInBalancePool(address _token, address[] memory _tokens) internal pure returns (uint256){ diff --git a/contracts/OnChainPricingMainnetLenient.sol b/contracts/OnChainPricingMainnetLenient.sol index ed1cbbe..da63abe 100644 --- a/contracts/OnChainPricingMainnetLenient.sol +++ b/contracts/OnChainPricingMainnetLenient.sol @@ -32,7 +32,7 @@ contract OnChainPricingMainnetLenient is OnChainPricingMainnet { uint256 public slippage = 200; // 2% Initially - constructor(address _uniV3Simulator) OnChainPricingMainnet(_uniV3Simulator){ + constructor(address _uniV3Simulator, address _balancerV2Simulator) OnChainPricingMainnet(_uniV3Simulator, _balancerV2Simulator){ } diff --git a/contracts/UniV3SwapSimulator.sol b/contracts/UniV3SwapSimulator.sol index 8e1f5df..2f38381 100644 --- a/contracts/UniV3SwapSimulator.sol +++ b/contracts/UniV3SwapSimulator.sol @@ -44,8 +44,6 @@ contract UniV3SwapSimulator { using LowGasSafeMath for int256; using SafeCast for uint256; using SafeCast for int256; - - uint256 public constant MAX_BPS = 10000; /// @dev View function which aims to simplify Uniswap V3 swap logic (no oracle/fee update, etc) to /// @dev estimate the expected output for given swap parameters and slippage diff --git a/contracts/libraries/balancer/BalancerFixedPoint.sol b/contracts/libraries/balancer/BalancerFixedPoint.sol new file mode 100644 index 0000000..e08743f --- /dev/null +++ b/contracts/libraries/balancer/BalancerFixedPoint.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.7.6; +pragma abicoder v2; + +import "./BalancerLogExpMath.sol"; + +// https://github.com/balancer-labs/balancer-v2-monorepo/blob/master/pkg/solidity-utils/contracts/math/FixedPoint.sol +library BalancerFixedPoint { + uint256 internal constant ONE = 1e18; // 18 decimal places + uint256 internal constant TWO = 2 * ONE; + uint256 internal constant FOUR = 4 * ONE; + uint256 internal constant MAX_POW_RELATIVE_ERROR = 10000; // 10^(-14) + + function add(uint256 a, uint256 b) internal pure returns (uint256) { + uint256 c = a + b; + require(c >= a, '!add'); + return c; + } + + function divUp(uint256 a, uint256 b) internal pure returns (uint256) { + require(b != 0, '!b0'); + + if (a == 0) { + return 0; + } else { + uint256 aInflated = a * ONE; + require(aInflated / a == ONE, '!divU'); // mul overflow + + // The traditional divUp formula is: + // divUp(x, y) := (x + y - 1) / y + // To avoid intermediate overflow in the addition, we distribute the division and get: + // divUp(x, y) := (x - 1) / y + 1 + // Note that this requires x != 0, which we already tested for. + + return ((aInflated - 1) / b) + 1; + } + } + + function divDown(uint256 a, uint256 b) internal pure returns (uint256) { + require(b != 0, '!b0'); + + if (a == 0) { + return 0; + } else { + uint256 aInflated = a * ONE; + require(aInflated / a == ONE, 'divD'); // mul overflow + + return aInflated / b; + } + } + + function mulUp(uint256 a, uint256 b) internal pure returns (uint256) { + uint256 product = a * b; + require(a == 0 || product / a == b, '!mul'); + + if (product == 0) { + return 0; + } else { + // The traditional divUp formula is: + // divUp(x, y) := (x + y - 1) / y + // To avoid intermediate overflow in the addition, we distribute the division and get: + // divUp(x, y) := (x - 1) / y + 1 + // Note that this requires x != 0, which we already tested for. + + return ((product - 1) / ONE) + 1; + } + } + + /** + * @dev Returns x^y, assuming both are fixed point numbers, rounding up. The result is guaranteed to not be below + * the true value (that is, the error function expected - actual is always negative). + */ + function powUp(uint256 x, uint256 y) internal pure returns (uint256) { + // Optimize for when y equals 1.0, 2.0 or 4.0, as those are very simple to implement and occur often in 50/50 + // and 80/20 Weighted Pools + if (y == ONE) { + return x; + } else if (y == TWO) { + return mulUp(x, x); + } else if (y == FOUR) { + uint256 square = mulUp(x, x); + return mulUp(square, square); + } else { + uint256 raw = BalancerLogExpMath.pow(x, y); + uint256 maxError = add(mulUp(raw, MAX_POW_RELATIVE_ERROR), 1); + + return add(raw, maxError); + } + } + + function mulDown(uint256 a, uint256 b) internal pure returns (uint256) { + uint256 product = a * b; + require(a == 0 || product / a == b, 'mulD'); + + return product / ONE; + } + + /** + * @dev Returns the complement of a value (1 - x), capped to 0 if x is larger than 1. + * + * Useful when computing the complement for values with some level of relative error, as it strips this error and + * prevents intermediate negative values. + */ + function complement(uint256 x) internal pure returns (uint256) { + return (x < ONE) ? (ONE - x) : 0; + } +} \ No newline at end of file diff --git a/contracts/libraries/balancer/BalancerLogExpMath.sol b/contracts/libraries/balancer/BalancerLogExpMath.sol new file mode 100644 index 0000000..8fbafb5 --- /dev/null +++ b/contracts/libraries/balancer/BalancerLogExpMath.sol @@ -0,0 +1,490 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.7.6; +pragma abicoder v2; + +// https://github.com/balancer-labs/balancer-v2-monorepo/blob/master/pkg/solidity-utils/contracts/math/LogExpMath.sol +library BalancerLogExpMath { + // All fixed point multiplications and divisions are inlined. This means we need to divide by ONE when multiplying + // two numbers, and multiply by ONE when dividing them. + + // All arguments and return values are 18 decimal fixed point numbers. + int256 constant ONE_18 = 1e18; + + // Internally, intermediate values are computed with higher precision as 20 decimal fixed point numbers, and in the + // case of ln36, 36 decimals. + int256 constant ONE_20 = 1e20; + int256 constant ONE_36 = 1e36; + + // The domain of natural exponentiation is bound by the word size and number of decimals used. + // + // Because internally the result will be stored using 20 decimals, the largest possible result is + // (2^255 - 1) / 10^20, which makes the largest exponent ln((2^255 - 1) / 10^20) = 130.700829182905140221. + // The smallest possible result is 10^(-18), which makes largest negative argument + // ln(10^(-18)) = -41.446531673892822312. + // We use 130.0 and -41.0 to have some safety margin. + int256 constant MAX_NATURAL_EXPONENT = 130e18; + int256 constant MIN_NATURAL_EXPONENT = -41e18; + + // Bounds for ln_36's argument. Both ln(0.9) and ln(1.1) can be represented with 36 decimal places in a fixed point + // 256 bit integer. + int256 constant LN_36_LOWER_BOUND = ONE_18 - 1e17; + int256 constant LN_36_UPPER_BOUND = ONE_18 + 1e17; + + uint256 constant MILD_EXPONENT_BOUND = 2**254 / uint256(ONE_20); + + // 18 decimal constants + int256 constant x0 = 128000000000000000000; // 2ˆ7 + int256 constant a0 = 38877084059945950922200000000000000000000000000000000000; // eˆ(x0) (no decimals) + int256 constant x1 = 64000000000000000000; // 2ˆ6 + int256 constant a1 = 6235149080811616882910000000; // eˆ(x1) (no decimals) + + // 20 decimal constants + int256 constant x2 = 3200000000000000000000; // 2ˆ5 + int256 constant a2 = 7896296018268069516100000000000000; // eˆ(x2) + int256 constant x3 = 1600000000000000000000; // 2ˆ4 + int256 constant a3 = 888611052050787263676000000; // eˆ(x3) + int256 constant x4 = 800000000000000000000; // 2ˆ3 + int256 constant a4 = 298095798704172827474000; // eˆ(x4) + int256 constant x5 = 400000000000000000000; // 2ˆ2 + int256 constant a5 = 5459815003314423907810; // eˆ(x5) + int256 constant x6 = 200000000000000000000; // 2ˆ1 + int256 constant a6 = 738905609893065022723; // eˆ(x6) + int256 constant x7 = 100000000000000000000; // 2ˆ0 + int256 constant a7 = 271828182845904523536; // eˆ(x7) + int256 constant x8 = 50000000000000000000; // 2ˆ-1 + int256 constant a8 = 164872127070012814685; // eˆ(x8) + int256 constant x9 = 25000000000000000000; // 2ˆ-2 + int256 constant a9 = 128402541668774148407; // eˆ(x9) + int256 constant x10 = 12500000000000000000; // 2ˆ-3 + int256 constant a10 = 113314845306682631683; // eˆ(x10) + int256 constant x11 = 6250000000000000000; // 2ˆ-4 + int256 constant a11 = 106449445891785942956; // eˆ(x11) + + /** + * @dev Exponentiation (x^y) with unsigned 18 decimal fixed point base and exponent. + * + * Reverts if ln(x) * y is smaller than `MIN_NATURAL_EXPONENT`, or larger than `MAX_NATURAL_EXPONENT`. + */ + function pow(uint256 x, uint256 y) internal pure returns (uint256) { + if (y == 0) { + // We solve the 0^0 indetermination by making it equal one. + return uint256(ONE_18); + } + + if (x == 0) { + return 0; + } + + // Instead of computing x^y directly, we instead rely on the properties of logarithms and exponentiation to + // arrive at that result. In particular, exp(ln(x)) = x, and ln(x^y) = y * ln(x). This means + // x^y = exp(y * ln(x)). + + // The ln function takes a signed value, so we need to make sure x fits in the signed 256 bit range. + require(x >> 255 == 0, '!OUTB'); + int256 x_int256 = int256(x); + + // We will compute y * ln(x) in a single step. Depending on the value of x, we can either use ln or ln_36. In + // both cases, we leave the division by ONE_18 (due to fixed point multiplication) to the end. + + // This prevents y * ln(x) from overflowing, and at the same time guarantees y fits in the signed 256 bit range. + require(y < MILD_EXPONENT_BOUND, '!OUTB'); + int256 y_int256 = int256(y); + + int256 logx_times_y; + if (LN_36_LOWER_BOUND < x_int256 && x_int256 < LN_36_UPPER_BOUND) { + int256 ln_36_x = _ln_36(x_int256); + + // ln_36_x has 36 decimal places, so multiplying by y_int256 isn't as straightforward, since we can't just + // bring y_int256 to 36 decimal places, as it might overflow. Instead, we perform two 18 decimal + // multiplications and add the results: one with the first 18 decimals of ln_36_x, and one with the + // (downscaled) last 18 decimals. + logx_times_y = ((ln_36_x / ONE_18) * y_int256 + ((ln_36_x % ONE_18) * y_int256) / ONE_18); + } else { + logx_times_y = _ln(x_int256) * y_int256; + } + logx_times_y /= ONE_18; + + // Finally, we compute exp(y * ln(x)) to arrive at x^y + require( + MIN_NATURAL_EXPONENT <= logx_times_y && logx_times_y <= MAX_NATURAL_EXPONENT, + '!OUTB' + ); + + return uint256(exp(logx_times_y)); + } + + /** + * @dev Natural exponentiation (e^x) with signed 18 decimal fixed point exponent. + * + * Reverts if `x` is smaller than MIN_NATURAL_EXPONENT, or larger than `MAX_NATURAL_EXPONENT`. + */ + function exp(int256 x) internal pure returns (int256) { + require(x >= MIN_NATURAL_EXPONENT && x <= MAX_NATURAL_EXPONENT, '!EXP'); + + if (x < 0) { + // We only handle positive exponents: e^(-x) is computed as 1 / e^x. We can safely make x positive since it + // fits in the signed 256 bit range (as it is larger than MIN_NATURAL_EXPONENT). + // Fixed point division requires multiplying by ONE_18. + return ((ONE_18 * ONE_18) / exp(-x)); + } + + // First, we use the fact that e^(x+y) = e^x * e^y to decompose x into a sum of powers of two, which we call x_n, + // where x_n == 2^(7 - n), and e^x_n = a_n has been precomputed. We choose the first x_n, x0, to equal 2^7 + // because all larger powers are larger than MAX_NATURAL_EXPONENT, and therefore not present in the + // decomposition. + // At the end of this process we will have the product of all e^x_n = a_n that apply, and the remainder of this + // decomposition, which will be lower than the smallest x_n. + // exp(x) = k_0 * a_0 * k_1 * a_1 * ... + k_n * a_n * exp(remainder), where each k_n equals either 0 or 1. + // We mutate x by subtracting x_n, making it the remainder of the decomposition. + + // The first two a_n (e^(2^7) and e^(2^6)) are too large if stored as 18 decimal numbers, and could cause + // intermediate overflows. Instead we store them as plain integers, with 0 decimals. + // Additionally, x0 + x1 is larger than MAX_NATURAL_EXPONENT, which means they will not both be present in the + // decomposition. + + // For each x_n, we test if that term is present in the decomposition (if x is larger than it), and if so deduct + // it and compute the accumulated product. + + int256 firstAN; + if (x >= x0) { + x -= x0; + firstAN = a0; + } else if (x >= x1) { + x -= x1; + firstAN = a1; + } else { + firstAN = 1; // One with no decimal places + } + + // We now transform x into a 20 decimal fixed point number, to have enhanced precision when computing the + // smaller terms. + x *= 100; + + // `product` is the accumulated product of all a_n (except a0 and a1), which starts at 20 decimal fixed point + // one. Recall that fixed point multiplication requires dividing by ONE_20. + int256 product = ONE_20; + + if (x >= x2) { + x -= x2; + product = (product * a2) / ONE_20; + } + if (x >= x3) { + x -= x3; + product = (product * a3) / ONE_20; + } + if (x >= x4) { + x -= x4; + product = (product * a4) / ONE_20; + } + if (x >= x5) { + x -= x5; + product = (product * a5) / ONE_20; + } + if (x >= x6) { + x -= x6; + product = (product * a6) / ONE_20; + } + if (x >= x7) { + x -= x7; + product = (product * a7) / ONE_20; + } + if (x >= x8) { + x -= x8; + product = (product * a8) / ONE_20; + } + if (x >= x9) { + x -= x9; + product = (product * a9) / ONE_20; + } + + // x10 and x11 are unnecessary here since we have high enough precision already. + + // Now we need to compute e^x, where x is small (in particular, it is smaller than x9). We use the Taylor series + // expansion for e^x: 1 + x + (x^2 / 2!) + (x^3 / 3!) + ... + (x^n / n!). + + int256 seriesSum = ONE_20; // The initial one in the sum, with 20 decimal places. + int256 term; // Each term in the sum, where the nth term is (x^n / n!). + + // The first term is simply x. + term = x; + seriesSum += term; + + // Each term (x^n / n!) equals the previous one times x, divided by n. Since x is a fixed point number, + // multiplying by it requires dividing by ONE_20, but dividing by the non-fixed point n values does not. + + term = ((term * x) / ONE_20) / 2; + seriesSum += term; + + term = ((term * x) / ONE_20) / 3; + seriesSum += term; + + term = ((term * x) / ONE_20) / 4; + seriesSum += term; + + term = ((term * x) / ONE_20) / 5; + seriesSum += term; + + term = ((term * x) / ONE_20) / 6; + seriesSum += term; + + term = ((term * x) / ONE_20) / 7; + seriesSum += term; + + term = ((term * x) / ONE_20) / 8; + seriesSum += term; + + term = ((term * x) / ONE_20) / 9; + seriesSum += term; + + term = ((term * x) / ONE_20) / 10; + seriesSum += term; + + term = ((term * x) / ONE_20) / 11; + seriesSum += term; + + term = ((term * x) / ONE_20) / 12; + seriesSum += term; + + // 12 Taylor terms are sufficient for 18 decimal precision. + + // We now have the first a_n (with no decimals), and the product of all other a_n present, and the Taylor + // approximation of the exponentiation of the remainder (both with 20 decimals). All that remains is to multiply + // all three (one 20 decimal fixed point multiplication, dividing by ONE_20, and one integer multiplication), + // and then drop two digits to return an 18 decimal value. + + return (((product * seriesSum) / ONE_20) * firstAN) / 100; + } + + /** + * @dev Logarithm (log(arg, base), with signed 18 decimal fixed point base and argument. + */ + function log(int256 arg, int256 base) internal pure returns (int256) { + // This performs a simple base change: log(arg, base) = ln(arg) / ln(base). + + // Both logBase and logArg are computed as 36 decimal fixed point numbers, either by using ln_36, or by + // upscaling. + + int256 logBase; + if (LN_36_LOWER_BOUND < base && base < LN_36_UPPER_BOUND) { + logBase = _ln_36(base); + } else { + logBase = _ln(base) * ONE_18; + } + + int256 logArg; + if (LN_36_LOWER_BOUND < arg && arg < LN_36_UPPER_BOUND) { + logArg = _ln_36(arg); + } else { + logArg = _ln(arg) * ONE_18; + } + + // When dividing, we multiply by ONE_18 to arrive at a result with 18 decimal places + return (logArg * ONE_18) / logBase; + } + + /** + * @dev Natural logarithm (ln(a)) with signed 18 decimal fixed point argument. + */ + function ln(int256 a) internal pure returns (int256) { + // The real natural logarithm is not defined for negative numbers or zero. + require(a > 0, '!OUTB'); + if (LN_36_LOWER_BOUND < a && a < LN_36_UPPER_BOUND) { + return _ln_36(a) / ONE_18; + } else { + return _ln(a); + } + } + + /** + * @dev Internal natural logarithm (ln(a)) with signed 18 decimal fixed point argument. + */ + function _ln(int256 a) private pure returns (int256) { + if (a < ONE_18) { + // Since ln(a^k) = k * ln(a), we can compute ln(a) as ln(a) = ln((1/a)^(-1)) = - ln((1/a)). If a is less + // than one, 1/a will be greater than one, and this if statement will not be entered in the recursive call. + // Fixed point division requires multiplying by ONE_18. + return (-_ln((ONE_18 * ONE_18) / a)); + } + + // First, we use the fact that ln^(a * b) = ln(a) + ln(b) to decompose ln(a) into a sum of powers of two, which + // we call x_n, where x_n == 2^(7 - n), which are the natural logarithm of precomputed quantities a_n (that is, + // ln(a_n) = x_n). We choose the first x_n, x0, to equal 2^7 because the exponential of all larger powers cannot + // be represented as 18 fixed point decimal numbers in 256 bits, and are therefore larger than a. + // At the end of this process we will have the sum of all x_n = ln(a_n) that apply, and the remainder of this + // decomposition, which will be lower than the smallest a_n. + // ln(a) = k_0 * x_0 + k_1 * x_1 + ... + k_n * x_n + ln(remainder), where each k_n equals either 0 or 1. + // We mutate a by subtracting a_n, making it the remainder of the decomposition. + + // For reasons related to how `exp` works, the first two a_n (e^(2^7) and e^(2^6)) are not stored as fixed point + // numbers with 18 decimals, but instead as plain integers with 0 decimals, so we need to multiply them by + // ONE_18 to convert them to fixed point. + // For each a_n, we test if that term is present in the decomposition (if a is larger than it), and if so divide + // by it and compute the accumulated sum. + + int256 sum = 0; + if (a >= a0 * ONE_18) { + a /= a0; // Integer, not fixed point division + sum += x0; + } + + if (a >= a1 * ONE_18) { + a /= a1; // Integer, not fixed point division + sum += x1; + } + + // All other a_n and x_n are stored as 20 digit fixed point numbers, so we convert the sum and a to this format. + sum *= 100; + a *= 100; + + // Because further a_n are 20 digit fixed point numbers, we multiply by ONE_20 when dividing by them. + + if (a >= a2) { + a = (a * ONE_20) / a2; + sum += x2; + } + + if (a >= a3) { + a = (a * ONE_20) / a3; + sum += x3; + } + + if (a >= a4) { + a = (a * ONE_20) / a4; + sum += x4; + } + + if (a >= a5) { + a = (a * ONE_20) / a5; + sum += x5; + } + + if (a >= a6) { + a = (a * ONE_20) / a6; + sum += x6; + } + + if (a >= a7) { + a = (a * ONE_20) / a7; + sum += x7; + } + + if (a >= a8) { + a = (a * ONE_20) / a8; + sum += x8; + } + + if (a >= a9) { + a = (a * ONE_20) / a9; + sum += x9; + } + + if (a >= a10) { + a = (a * ONE_20) / a10; + sum += x10; + } + + if (a >= a11) { + a = (a * ONE_20) / a11; + sum += x11; + } + + // a is now a small number (smaller than a_11, which roughly equals 1.06). This means we can use a Taylor series + // that converges rapidly for values of `a` close to one - the same one used in ln_36. + // Let z = (a - 1) / (a + 1). + // ln(a) = 2 * (z + z^3 / 3 + z^5 / 5 + z^7 / 7 + ... + z^(2 * n + 1) / (2 * n + 1)) + + // Recall that 20 digit fixed point division requires multiplying by ONE_20, and multiplication requires + // division by ONE_20. + int256 z = ((a - ONE_20) * ONE_20) / (a + ONE_20); + int256 z_squared = (z * z) / ONE_20; + + // num is the numerator of the series: the z^(2 * n + 1) term + int256 num = z; + + // seriesSum holds the accumulated sum of each term in the series, starting with the initial z + int256 seriesSum = num; + + // In each step, the numerator is multiplied by z^2 + num = (num * z_squared) / ONE_20; + seriesSum += num / 3; + + num = (num * z_squared) / ONE_20; + seriesSum += num / 5; + + num = (num * z_squared) / ONE_20; + seriesSum += num / 7; + + num = (num * z_squared) / ONE_20; + seriesSum += num / 9; + + num = (num * z_squared) / ONE_20; + seriesSum += num / 11; + + // 6 Taylor terms are sufficient for 36 decimal precision. + + // Finally, we multiply by 2 (non fixed point) to compute ln(remainder) + seriesSum *= 2; + + // We now have the sum of all x_n present, and the Taylor approximation of the logarithm of the remainder (both + // with 20 decimals). All that remains is to sum these two, and then drop two digits to return a 18 decimal + // value. + + return (sum + seriesSum) / 100; + } + + /** + * @dev Intrnal high precision (36 decimal places) natural logarithm (ln(x)) with signed 18 decimal fixed point argument, + * for x close to one. + * + * Should only be used if x is between LN_36_LOWER_BOUND and LN_36_UPPER_BOUND. + */ + function _ln_36(int256 x) private pure returns (int256) { + // Since ln(1) = 0, a value of x close to one will yield a very small result, which makes using 36 digits + // worthwhile. + + // First, we transform x to a 36 digit fixed point value. + x *= ONE_18; + + // We will use the following Taylor expansion, which converges very rapidly. Let z = (x - 1) / (x + 1). + // ln(x) = 2 * (z + z^3 / 3 + z^5 / 5 + z^7 / 7 + ... + z^(2 * n + 1) / (2 * n + 1)) + + // Recall that 36 digit fixed point division requires multiplying by ONE_36, and multiplication requires + // division by ONE_36. + int256 z = ((x - ONE_36) * ONE_36) / (x + ONE_36); + int256 z_squared = (z * z) / ONE_36; + + // num is the numerator of the series: the z^(2 * n + 1) term + int256 num = z; + + // seriesSum holds the accumulated sum of each term in the series, starting with the initial z + int256 seriesSum = num; + + // In each step, the numerator is multiplied by z^2 + num = (num * z_squared) / ONE_36; + seriesSum += num / 3; + + num = (num * z_squared) / ONE_36; + seriesSum += num / 5; + + num = (num * z_squared) / ONE_36; + seriesSum += num / 7; + + num = (num * z_squared) / ONE_36; + seriesSum += num / 9; + + num = (num * z_squared) / ONE_36; + seriesSum += num / 11; + + num = (num * z_squared) / ONE_36; + seriesSum += num / 13; + + num = (num * z_squared) / ONE_36; + seriesSum += num / 15; + + // 8 Taylor terms are sufficient for 36 decimal precision. + + // All that remains is multiplying by 2 (non fixed point). + return seriesSum * 2; + } + +} \ No newline at end of file diff --git a/interfaces/balancer/IBalancerV2Simulator.sol b/interfaces/balancer/IBalancerV2Simulator.sol new file mode 100644 index 0000000..8cc16a5 --- /dev/null +++ b/interfaces/balancer/IBalancerV2Simulator.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity 0.8.10; +pragma abicoder v2; + +struct ExactInQueryParam{ + uint256 balanceIn; + uint256 weightIn; + uint256 balanceOut; + uint256 weightOut; + uint256 amountIn; +} + +interface IBalancerV2Simulator { + function calcOutGivenIn(ExactInQueryParam memory _query) external pure returns (uint256); +} \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 33668d9..c0e0a31 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -41,13 +41,15 @@ def swapexecutor(): @pytest.fixture def pricer(): univ3simulator = UniV3SwapSimulator.deploy({"from": a[0]}) - return OnChainPricingMainnet.deploy(univ3simulator.address, {"from": a[0]}) + balancerV2Simulator = BalancerSwapSimulator.deploy({"from": a[0]}) + return OnChainPricingMainnet.deploy(univ3simulator.address, balancerV2Simulator.address, {"from": a[0]}) @pytest.fixture def lenient_contract(): ## NOTE: We have 5% slippage on this one univ3simulator = UniV3SwapSimulator.deploy({"from": a[0]}) - c = OnChainPricingMainnetLenient.deploy(univ3simulator.address, {"from": a[0]}) + balancerV2Simulator = BalancerSwapSimulator.deploy({"from": a[0]}) + c = OnChainPricingMainnetLenient.deploy(univ3simulator.address, balancerV2Simulator.address, {"from": a[0]}) c.setSlippage(499, {"from": accounts.at(c.TECH_OPS(), force=True)}) return c diff --git a/tests/gas_benchmark/benchmark_pricer_gas.py b/tests/gas_benchmark/benchmark_pricer_gas.py index 85ff6c0..776062d 100644 --- a/tests/gas_benchmark/benchmark_pricer_gas.py +++ b/tests/gas_benchmark/benchmark_pricer_gas.py @@ -39,7 +39,7 @@ def test_gas_only_balancer_v2(oneE18, weth, aura, pricer): tx = pricer.findOptimalSwap(token, weth.address, sell_amount) assert tx.return_value[0] == 5 ## BALANCER assert tx.return_value[1] > 0 - assert tx.gas_used <= 90000 ## 87843 in test simulation + assert tx.gas_used <= 95000 ## 91345 in test simulation def test_gas_only_balancer_v2_with_weth(oneE18, wbtc, aura, pricer): token = aura # some swap (AURA-WETH-WBTC) only in Balancer V2 via WETH in between as connector @@ -50,7 +50,7 @@ def test_gas_only_balancer_v2_with_weth(oneE18, wbtc, aura, pricer): tx = pricer.findOptimalSwap(token, wbtc.address, sell_amount) assert tx.return_value[0] == 6 ## BALANCERWITHWETH assert tx.return_value[1] > 0 - assert tx.gas_used <= 140000 ## 134538 in test simulation + assert tx.gas_used <= 145000 ## 141062 in test simulation def test_gas_only_uniswap_v3(oneE18, weth, pricer): token = "0xf4d2888d29D722226FafA5d9B24F9164c092421E" # some swap (LOOKS-WETH) only in Uniswap V3 diff --git a/tests/on_chain_pricer/test_balancer_pricer.py b/tests/on_chain_pricer/test_balancer_pricer.py index 19ee4e1..f69d83b 100644 --- a/tests/on_chain_pricer/test_balancer_pricer.py +++ b/tests/on_chain_pricer/test_balancer_pricer.py @@ -84,9 +84,9 @@ def test_get_balancer_price_with_connector_analytical(oneE18, wbtc, usdc, weth, def test_get_balancer_price_ohm_analytical(oneE18, ohm, dai, pricer): ## 1e8 sell_count = 1000 - sell_amount = sell_count * oneE18 + sell_amount = sell_count * 1000000000 ## 1e9 - ## minimum quote for OHM in DAI(1e6) + ## minimum quote for OHM in DAI(1e18) p = sell_count * 10 * oneE18 quote = pricer.getBalancerPriceAnalytically(ohm.address, sell_amount, dai.address) assert quote >= p From 9b24a413602d54992585c15e5b5c3e698af232f4 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Wed, 20 Jul 2022 23:42:28 +0200 Subject: [PATCH 11/53] feat: back to `view` function --- contracts/OnChainPricingMainnet.sol | 4 ++-- contracts/OnChainPricingMainnetLenient.sol | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/OnChainPricingMainnet.sol b/contracts/OnChainPricingMainnet.sol index 1f32208..2300e02 100644 --- a/contracts/OnChainPricingMainnet.sol +++ b/contracts/OnChainPricingMainnet.sol @@ -175,12 +175,12 @@ contract OnChainPricingMainnet { } /// @dev External function, virtual so you can override, see Lenient Version - function findOptimalSwap(address tokenIn, address tokenOut, uint256 amountIn) external virtual returns (Quote memory) { + function findOptimalSwap(address tokenIn, address tokenOut, uint256 amountIn) external virtual view returns (Quote memory) { return _findOptimalSwap(tokenIn, tokenOut, amountIn); } /// @dev View function for testing the routing of the strategy - function _findOptimalSwap(address tokenIn, address tokenOut, uint256 amountIn) internal returns (Quote memory) { + function _findOptimalSwap(address tokenIn, address tokenOut, uint256 amountIn) internal view returns (Quote memory) { bool wethInvolved = (tokenIn == WETH || tokenOut == WETH); uint256 length = wethInvolved? 5 : 7; // Add length you need diff --git a/contracts/OnChainPricingMainnetLenient.sol b/contracts/OnChainPricingMainnetLenient.sol index da63abe..362be14 100644 --- a/contracts/OnChainPricingMainnetLenient.sol +++ b/contracts/OnChainPricingMainnetLenient.sol @@ -45,7 +45,7 @@ contract OnChainPricingMainnetLenient is OnChainPricingMainnet { // === PRICING === // /// @dev View function for testing the routing of the strategy - function findOptimalSwap(address tokenIn, address tokenOut, uint256 amountIn) external override returns (Quote memory q) { + function findOptimalSwap(address tokenIn, address tokenOut, uint256 amountIn) external view override returns (Quote memory q) { q = _findOptimalSwap(tokenIn, tokenOut, amountIn); q.amountOut = q.amountOut * (MAX_BPS - slippage) / MAX_BPS; } From df59873c5ea3d9494767bde3e14ff6e5145c02b4 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Wed, 20 Jul 2022 23:49:56 +0200 Subject: [PATCH 12/53] fix: `view` test changes --- README.md | 6 +++--- tests/on_chain_pricer/test_balancer_pricer.py | 6 +++--- tests/on_chain_pricer/test_bribe_tokens_supported.py | 4 ++-- tests/on_chain_pricer/test_swap_exec_on_chain.py | 8 ++++---- tests/on_chain_pricer/test_univ3_pricer.py | 4 ++-- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 347361c..02cb75e 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Covering >80% TVL on Mainnet. ## Example Usage -NOTE: Because of Balancer and UniV3 (go bug their devs pls), the following functions are not view, you must `.call` them from offchain to avoid spending gas +BREAKING CHANGE: V3 is back to `view` even for Balancer and UniV3 functions ### isPairSupported @@ -45,7 +45,7 @@ NOTE: This is not proof of optimality In Brownie ```python -quote = pricer.isPairSupported.call(t_in, t_out, amt_in) ## Add .call to avoid paying for the tx +quote = pricer.isPairSupported(t_in, t_out, amt_in) ``` ### findOptimalSwap @@ -59,7 +59,7 @@ NOTE: While the function says optimal, this is not optimal, just best of the bun In Brownie ```python -quote = pricer.findOptimalSwap.call(t_in, t_out, amt_in) ## Add .call to avoid paying for the tx +quote = pricer.findOptimalSwap(t_in, t_out, amt_in) ``` diff --git a/tests/on_chain_pricer/test_balancer_pricer.py b/tests/on_chain_pricer/test_balancer_pricer.py index f69d83b..531a09f 100644 --- a/tests/on_chain_pricer/test_balancer_pricer.py +++ b/tests/on_chain_pricer/test_balancer_pricer.py @@ -15,7 +15,7 @@ def test_get_balancer_price(oneE18, weth, usdc, pricer): ## minimum quote for ETH in USDC(1e6) p = 1 * 500 * 1000000 - quote = pricer.getBalancerPrice(weth.address, sell_amount, usdc.address).return_value + quote = pricer.getBalancerPrice(weth.address, sell_amount, usdc.address) assert quote >= p ## price sanity check with fine liquidity @@ -33,7 +33,7 @@ def test_get_balancer_price_with_connector(oneE18, wbtc, usdc, weth, pricer): ## minimum quote for WBTC in USDC(1e6) p = sell_count * 10000 * 1000000 - quote = pricer.getBalancerPriceWithConnector(wbtc.address, sell_amount, usdc.address, weth.address).return_value + quote = pricer.getBalancerPriceWithConnector(wbtc.address, sell_amount, usdc.address, weth.address) assert quote >= p ## price sanity check with dime liquidity @@ -50,7 +50,7 @@ def test_get_balancer_price_nonexistence(oneE18, cvx, weth, pricer): sell_amount = 100 * oneE18 ## no proper pool in Balancer for WETH in CVX - quote = pricer.getBalancerPrice(weth.address, sell_amount, cvx.address).return_value + quote = pricer.getBalancerPrice(weth.address, sell_amount, cvx.address) assert quote == 0 """ diff --git a/tests/on_chain_pricer/test_bribe_tokens_supported.py b/tests/on_chain_pricer/test_bribe_tokens_supported.py index ba9389a..d9a9f1e 100644 --- a/tests/on_chain_pricer/test_bribe_tokens_supported.py +++ b/tests/on_chain_pricer/test_bribe_tokens_supported.py @@ -56,7 +56,7 @@ def test_are_bribes_supported(pricer, token): ## 1e18 for everything, even with insane slippage will still return non-zero which is sufficient at this time AMOUNT = 1e18 - res = pricer.isPairSupported(token, WETH, AMOUNT).return_value + res = pricer.isPairSupported(token, WETH, AMOUNT) assert res @pytest.mark.parametrize("token", TOKENS_18_DECIMALS) @@ -69,7 +69,7 @@ def test_bribes_get_optimal_quote(pricer, token): ## 1e18 for everything, even with insane slippage will still return non-zero which is sufficient at this time AMOUNT = 1e18 - res = pricer.findOptimalSwap(token, WETH, AMOUNT).return_value + res = pricer.findOptimalSwap(token, WETH, AMOUNT) assert res[1] > 0 diff --git a/tests/on_chain_pricer/test_swap_exec_on_chain.py b/tests/on_chain_pricer/test_swap_exec_on_chain.py index 526ac30..d643439 100644 --- a/tests/on_chain_pricer/test_swap_exec_on_chain.py +++ b/tests/on_chain_pricer/test_swap_exec_on_chain.py @@ -59,7 +59,7 @@ def test_swap_in_univ3_single(oneE18, wbtc_whale, wbtc, usdc, pricer, swapexecut ## minimum quote for WBTC in USDC(1e6) p = 1 * 15000 * 1000000 - quote = pricer.findOptimalSwap(wbtc.address, usdc.address, sell_amount).return_value + quote = pricer.findOptimalSwap(wbtc.address, usdc.address, sell_amount) assert quote[1] >= p ## swap on chain @@ -82,7 +82,7 @@ def test_swap_in_univ3(oneE18, wbtc_whale, wbtc, weth, usdc, pricer, swapexecuto ## minimum quote for WBTC in USDC(1e6) p = 1 * 15000 * 1000000 - quote = pricer.findOptimalSwap(wbtc.address, usdc.address, sell_amount).return_value + quote = pricer.findOptimalSwap(wbtc.address, usdc.address, sell_amount) assert quote[1] >= p ## swap on chain @@ -105,7 +105,7 @@ def test_swap_in_balancer_batch(oneE18, wbtc_whale, wbtc, weth, usdc, pricer, sw ## minimum quote for WBTC in USDC(1e6) p = 1 * 15000 * 1000000 - quote = pricer.findOptimalSwap(wbtc.address, usdc.address, sell_amount).return_value + quote = pricer.findOptimalSwap(wbtc.address, usdc.address, sell_amount) assert quote[1] >= p ## swap on chain @@ -129,7 +129,7 @@ def test_swap_in_balancer_single(oneE18, weth_whale, weth, usdc, pricer, swapexe ## minimum quote for WETH in USDC(1e6) p = 1 * 500 * 1000000 - quote = pricer.findOptimalSwap(weth.address, usdc.address, sell_amount).return_value + quote = pricer.findOptimalSwap(weth.address, usdc.address, sell_amount) assert quote[1] >= p ## swap on chain diff --git a/tests/on_chain_pricer/test_univ3_pricer.py b/tests/on_chain_pricer/test_univ3_pricer.py index c86fd4f..9be1c79 100644 --- a/tests/on_chain_pricer/test_univ3_pricer.py +++ b/tests/on_chain_pricer/test_univ3_pricer.py @@ -17,7 +17,7 @@ def test_get_univ3_price_in_range(oneE18, weth, usdc, usdc_whale, pricer): assert quote[0] == quoteInRange[1] ## check against quoter - quoterP = interface.IV3Quoter(pricer.UNIV3_QUOTER()).quoteExactInputSingle(weth.address, usdc.address, quote[1], sell_amount, 0, {'from': usdc_whale.address}).return_value + quoterP = interface.IV3Quoter(pricer.UNIV3_QUOTER()).quoteExactInputSingle(weth.address, usdc.address, quote[1], sell_amount, 0, {'from': usdc_whale.address}) assert quoterP == quote[0] ## fee-0.05% pool is the chosen one among (0.05%, 0.3%, 1%)! @@ -39,7 +39,7 @@ def test_get_univ3_price_cross_tick(oneE18, weth, usdc, usdc_whale, pricer): assert quote[0] == quoteCrossTicks ## check against quoter - quoterP = interface.IV3Quoter(pricer.UNIV3_QUOTER()).quoteExactInputSingle(weth.address, usdc.address, quote[1], sell_amount, 0, {'from': usdc_whale.address}).return_value + quoterP = interface.IV3Quoter(pricer.UNIV3_QUOTER()).quoteExactInputSingle(weth.address, usdc.address, quote[1], sell_amount, 0, {'from': usdc_whale.address}) assert (abs(quoterP - quote[0]) / quoterP) <= 0.0015 ## thousandsth in quote diff for a millions-dollar-worth swap ## fee-0.05% pool is the chosen one among (0.05%, 0.3%, 1%)! From 614c2652969f1ffae8db7d0d2c128a12b828d88c Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Wed, 20 Jul 2022 23:52:19 +0200 Subject: [PATCH 13/53] feat: archive of V2 --- .../archive/FullOnChainPricingMainnet.sol | 463 ++++++++++++++++++ 1 file changed, 463 insertions(+) create mode 100644 contracts/archive/FullOnChainPricingMainnet.sol diff --git a/contracts/archive/FullOnChainPricingMainnet.sol b/contracts/archive/FullOnChainPricingMainnet.sol new file mode 100644 index 0000000..5687110 --- /dev/null +++ b/contracts/archive/FullOnChainPricingMainnet.sol @@ -0,0 +1,463 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.10; + + +import {IERC20} from "@oz/token/ERC20/IERC20.sol"; +import {IERC20Metadata} from "@oz/token/ERC20/extensions/IERC20Metadata.sol"; +import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol"; +import {Address} from "@oz/utils/Address.sol"; + + +import "../../interfaces/uniswap/IUniswapRouterV2.sol"; +import "../../interfaces/uniswap/IV3Pool.sol"; +import "../../interfaces/uniswap/IV3Quoter.sol"; +import "../../interfaces/balancer/IBalancerV2Vault.sol"; +import "../../interfaces/curve/ICurveRouter.sol"; +import "../../interfaces/curve/ICurvePool.sol"; + +enum SwapType { + CURVE, //0 + UNIV2, //1 + SUSHI, //2 + UNIV3, //3 + UNIV3WITHWETH, //4 + BALANCER, //5 + BALANCERWITHWETH //6 +} + +/// @title OnChainPricing +/// @author Alex the Entreprenerd for BadgerDAO +/// @author Camotelli @rayeaster +/// @dev Mainnet Version of Price Quoter, hardcoded for more efficiency +/// @notice Feature Complete, non gas optimized Mainnet Pricer +/// A complete quote will cost up to 1.6MLN gas. +/// This contract acts as a reference to a gas optimized version for V3 +contract FullOnChainPricingMainnet { + using Address for address; + + // Assumption #1 Most tokens liquid pair is WETH (WETH is tokenized ETH for that chain) + // e.g on Fantom, WETH would be wFTM + address public constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + + /// == Uni V2 Like Routers || These revert on non-existent pair == // + // UniV2 + address public constant UNIV2_ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; // Spookyswap + // Sushi + address public constant SUSHI_ROUTER = 0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F; + + // Curve / Doesn't revert on failure + address public constant CURVE_ROUTER = 0x8e764bE4288B842791989DB5b8ec067279829809; // Curve quote and swaps + + // UniV3 impl credit to https://github.com/1inch/spot-price-aggregator/blob/master/contracts/oracles/UniswapV3Oracle.sol + address public constant UNIV3_QUOTER = 0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6; + bytes32 public constant UNIV3_POOL_INIT_CODE_HASH = 0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54; + address public constant UNIV3_FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984; + uint24[4] univ3_fees = [uint24(100), 500, 3000, 10000]; + + // BalancerV2 Vault + address public constant BALANCERV2_VAULT = 0xBA12222222228d8Ba445958a75a0704d566BF2C8; + bytes32 public constant BALANCERV2_NONEXIST_POOLID = "BALANCER-V2-NON-EXIST-POOLID"; + // selected Balancer V2 pools for given pairs on Ethereum with liquidity > $5M: https://dev.balancer.fi/references/subgraphs#examples + bytes32 public constant BALANCERV2_WSTETH_WETH_POOLID = 0x32296969ef14eb0c6d29669c550d4a0449130230000200000000000000000080; + address public constant WSTETH = 0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0; + bytes32 public constant BALANCERV2_WBTC_WETH_POOLID = 0xa6f548df93de924d73be7d25dc02554c6bd66db500020000000000000000000e; + address public constant WBTC = 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599; + bytes32 public constant BALANCERV2_USDC_WETH_POOLID = 0x96646936b91d6b9d7d0c47c496afbf3d6ec7b6f8000200000000000000000019; + address public constant USDC = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; + bytes32 public constant BALANCERV2_BAL_WETH_POOLID = 0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014; + address public constant BAL = 0xba100000625a3754423978a60c9317c58a424e3D; + bytes32 public constant BALANCERV2_FEI_WETH_POOLID = 0x90291319f1d4ea3ad4db0dd8fe9e12baf749e84500020000000000000000013c; + address public constant FEI = 0x956F47F50A910163D8BF957Cf5846D573E7f87CA; + bytes32 public constant BALANCERV2_BADGER_WBTC_POOLID = 0xb460daa847c45f1c4a41cb05bfb3b51c92e41b36000200000000000000000194; + address public constant BADGER = 0x3472A5A71965499acd81997a54BBA8D852C6E53d; + bytes32 public constant BALANCERV2_GNO_WETH_POOLID = 0xf4c0dd9b82da36c07605df83c8a416f11724d88b000200000000000000000026; + address public constant GNO = 0x6810e776880C02933D47DB1b9fc05908e5386b96; + bytes32 public constant BALANCERV2_CREAM_WETH_POOLID = 0x85370d9e3bb111391cc89f6de344e801760461830002000000000000000001ef; + address public constant CREAM = 0x2ba592F78dB6436527729929AAf6c908497cB200; + bytes32 public constant BALANCERV2_LDO_WETH_POOLID = 0xbf96189eee9357a95c7719f4f5047f76bde804e5000200000000000000000087; + address public constant LDO = 0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32; + bytes32 public constant BALANCERV2_SRM_WETH_POOLID = 0x231e687c9961d3a27e6e266ac5c433ce4f8253e4000200000000000000000023; + address public constant SRM = 0x476c5E26a75bd202a9683ffD34359C0CC15be0fF; + bytes32 public constant BALANCERV2_rETH_WETH_POOLID = 0x1e19cf2d73a72ef1332c882f20534b6519be0276000200000000000000000112; + address public constant rETH = 0xae78736Cd615f374D3085123A210448E74Fc6393; + bytes32 public constant BALANCERV2_AKITA_WETH_POOLID = 0xc065798f227b49c150bcdc6cdc43149a12c4d75700020000000000000000010b; + address public constant AKITA = 0x3301Ee63Fb29F863f2333Bd4466acb46CD8323E6; + bytes32 public constant BALANCERV2_OHM_DAI_WETH_POOLID = 0xc45d42f801105e861e86658648e3678ad7aa70f900010000000000000000011e; + address public constant OHM = 0x64aa3364F17a4D01c6f1751Fd97C2BD3D7e7f1D5; + address public constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; + bytes32 public constant BALANCERV2_COW_WETH_POOLID = 0xde8c195aa41c11a0c4787372defbbddaa31306d2000200000000000000000181; + bytes32 public constant BALANCERV2_COW_GNO_POOLID = 0x92762b42a06dcdddc5b7362cfb01e631c4d44b40000200000000000000000182; + address public constant COW = 0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB; + bytes32 public constant BALANCERV2_AURA_WETH_POOLID = 0xc29562b045d80fd77c69bec09541f5c16fe20d9d000200000000000000000251; + address public constant AURA = 0xC0c293ce456fF0ED870ADd98a0828Dd4d2903DBF; + bytes32 public constant BALANCERV2_AURABAL_BALWETH_POOLID = 0x3dd0843a028c86e0b760b1a76929d1c5ef93a2dd000200000000000000000249; + + address public constant GRAVIAURA = 0xBA485b556399123261a5F9c95d413B4f93107407; + bytes32 public constant BALANCERV2_AURABAL_GRAVIAURA_BALWETH_POOLID = 0x0578292cb20a443ba1cde459c985ce14ca2bdee5000100000000000000000269; + + + address public constant AURABAL = 0x616e8BfA43F920657B3497DBf40D6b1A02D4608d; + address public constant BALWETHBPT = 0x5c6Ee304399DBdB9C8Ef030aB642B10820DB8F56; + uint256 public constant CURVE_FEE_SCALE = 100000; + + struct Quote { + SwapType name; + uint256 amountOut; + bytes32[] pools; // specific pools involved in the optimal swap path + uint256[] poolFees; // specific pool fees involved in the optimal swap path, typically in Uniswap V3 + } + + /// @dev Given tokenIn, out and amountIn, returns true if a quote will be non-zero + /// @notice Doesn't guarantee optimality, just non-zero + function isPairSupported(address tokenIn, address tokenOut, uint256 amountIn) external returns (bool) { + // Sorted by "assumed" reverse worst case + // Go for higher gas cost checks assuming they are offering best precision / good price + + // If There's a Bal Pool, since we have to hardcode, then the price is probably non-zero + bytes32 poolId = getBalancerV2Pool(tokenIn, tokenOut); + if (poolId != BALANCERV2_NONEXIST_POOLID){ + return true; + } + + // If no pool this is fairly cheap, else highly likely there's a price + if(getUniV3Price(tokenIn, amountIn, tokenOut) > 0) { + return true; + } + + // Highly likely to have any random token here + if(getUniPrice(UNIV2_ROUTER, tokenIn, tokenOut, amountIn) > 0) { + return true; + } + + // Otherwise it's probably on Sushi + if(getUniPrice(SUSHI_ROUTER, tokenIn, tokenOut, amountIn) > 0) { + return true; + } + + // Curve at this time has great execution prices but low selection + (address curvePool, uint256 curveQuote) = getCurvePrice(CURVE_ROUTER, tokenIn, tokenOut, amountIn); + if (curveQuote > 0){ + return true; + } + } + + /// @dev External function, virtual so you can override, see Lenient Version + function findOptimalSwap(address tokenIn, address tokenOut, uint256 amountIn) external virtual returns (Quote memory) { + return _findOptimalSwap(tokenIn, tokenOut, amountIn); + } + + /// @dev View function for testing the routing of the strategy + function _findOptimalSwap(address tokenIn, address tokenOut, uint256 amountIn) internal returns (Quote memory) { + bool wethInvolved = (tokenIn == WETH || tokenOut == WETH); + uint256 length = wethInvolved? 5 : 7; // Add length you need + + Quote[] memory quotes = new Quote[](length); + bytes32[] memory dummyPools; + uint256[] memory dummyPoolFees; + + (address curvePool, uint256 curveQuote) = getCurvePrice(CURVE_ROUTER, tokenIn, tokenOut, amountIn); + if (curveQuote > 0){ + (bytes32[] memory curvePools, uint256[] memory curvePoolFees) = _getCurveFees(curvePool); + quotes[0] = Quote(SwapType.CURVE, curveQuote, curvePools, curvePoolFees); + } else { + quotes[0] = Quote(SwapType.CURVE, curveQuote, dummyPools, dummyPoolFees); + } + + quotes[1] = Quote(SwapType.UNIV2, getUniPrice(UNIV2_ROUTER, tokenIn, tokenOut, amountIn), dummyPools, dummyPoolFees); + + quotes[2] = Quote(SwapType.SUSHI, getUniPrice(SUSHI_ROUTER, tokenIn, tokenOut, amountIn), dummyPools, dummyPoolFees); + + quotes[3] = Quote(SwapType.UNIV3, getUniV3Price(tokenIn, amountIn, tokenOut), dummyPools, dummyPoolFees); + + quotes[4] = Quote(SwapType.BALANCER, getBalancerPrice(tokenIn, amountIn, tokenOut), dummyPools, dummyPoolFees); + + if(!wethInvolved){ + quotes[5] = Quote(SwapType.UNIV3WITHWETH, getUniV3PriceWithConnector(tokenIn, amountIn, tokenOut, WETH), dummyPools, dummyPoolFees); + + quotes[6] = Quote(SwapType.BALANCERWITHWETH, getBalancerPriceWithConnector(tokenIn, amountIn, tokenOut, WETH), dummyPools, dummyPoolFees); + } + + // Because this is a generalized contract, it is best to just loop, + // Ideally we have a hierarchy for each chain to save some extra gas, but I think it's ok + // O(n) complexity and each check is like 9 gas + Quote memory bestQuote = quotes[0]; + unchecked { + for(uint256 x = 1; x < length; ++x) { + if(quotes[x].amountOut > bestQuote.amountOut) { + bestQuote = quotes[x]; + } + } + } + + + return bestQuote; + } + + /// === Component Functions === /// + /// Why bother? + /// Because each chain is slightly different but most use similar tech / forks + /// May as well use the separate functoions so each OnChain Pricing on different chains will be slightly different + /// But ultimately will work in the same way + + /// === UNIV2 === /// + + /// @dev Given the address of the UniV2Like Router, the input amount, and the path, returns the quote for it + function getUniPrice(address router, address tokenIn, address tokenOut, uint256 amountIn) public view returns (uint256) { + address[] memory path = new address[](2); + path[0] = address(tokenIn); + path[1] = address(tokenOut); + + uint256 quote; //0 + + + // TODO: Consider doing check before revert to avoid paying extra gas + // Specifically, test gas if we get revert vs if we check to avoid it + try IUniswapRouterV2(router).getAmountsOut(amountIn, path) returns (uint256[] memory amounts) { + quote = amounts[amounts.length - 1]; // Last one is the outToken + } catch (bytes memory) { + // We ignore as it means it's zero + } + + return quote; + } + + /// === UNIV3 === /// + + /// @dev Given the address of the input token & amount & the output token + /// @return the quote for it + function getUniV3Price(address tokenIn, uint256 amountIn, address tokenOut) public returns (uint256) { + uint256 quoteRate; + + (address token0, address token1, bool token0Price) = _ifUniV3Token0Price(tokenIn, tokenOut); + uint256 feeTypes = univ3_fees.length; + for (uint256 i = 0; i < feeTypes; ){ + //filter out disqualified pools to save gas on quoter swap query + uint256 rate = _getUniV3Rate(token0, token1, univ3_fees[i], token0Price, amountIn); + if (rate > 0){ + uint256 quote = _getUniV3QuoterQuery(tokenIn, tokenOut, univ3_fees[i], amountIn); + if (quote > quoteRate){ + quoteRate = quote; + } + } + + unchecked { ++i; } + } + + return quoteRate; + } + + /// @dev Given the address of the input token & amount & the output token & connector token in between (input token ---> connector token ---> output token) + /// @return the quote for it + function getUniV3PriceWithConnector(address tokenIn, uint256 amountIn, address tokenOut, address connectorToken) public returns (uint256) { + uint256 connectorAmount = getUniV3Price(tokenIn, amountIn, connectorToken); + if (connectorAmount > 0){ + return getUniV3Price(connectorToken, connectorAmount, tokenOut); + } else{ + return 0; + } + } + + /// @dev query swap result from Uniswap V3 quoter for given tokenIn -> tokenOut with amountIn & fee + function _getUniV3QuoterQuery(address tokenIn, address tokenOut, uint24 fee, uint256 amountIn) internal returns (uint256){ + uint256 quote = IV3Quoter(UNIV3_QUOTER).quoteExactInputSingle(tokenIn, tokenOut, fee, amountIn, 0); + return quote; + } + + /// @dev return token0 & token1 and if token0 equals tokenIn + function _ifUniV3Token0Price(address tokenIn, address tokenOut) internal pure returns (address, address, bool){ + (address token0, address token1) = tokenIn < tokenOut ? (tokenIn, tokenOut) : (tokenOut, tokenIn); + return (token0, token1, token0 == tokenIn); + } + + /// @dev Given the address of the input token & the output token & fee tier + /// @dev with trade amount & indicator if token0 pricing required (token1/token0 e.g., token0 -> token1) + /// @dev note there are some heuristic checks around the price like pool reserve should satisfy the swap amount + /// @return the current price in V3 for it + function _getUniV3Rate(address token0, address token1, uint24 fee, bool token0Price, uint256 amountIn) internal view returns (uint256) { + + // heuristic check0: ensure the pool [exist] and properly initiated + address pool = _getUniV3PoolAddress(token0, token1, fee); + if (!pool.isContract() || IUniswapV3Pool(pool).liquidity() == 0) { + return 0; + } + + // heuristic check1: ensure the pool tokenIn reserve makes sense in terms of [amountIn] + if (IERC20(token0Price? token0 : token1).balanceOf(pool) <= amountIn){ + return 0; + } + + // heuristic check2: ensure the pool tokenOut reserve makes sense in terms of the [amountOutput based on slot0 price] + uint256 rate = _queryUniV3PriceWithSlot(token0, token1, pool, token0Price); + uint256 amountOutput = rate * amountIn * (10 ** IERC20Metadata(token0Price? token1 : token0).decimals()) / (10 ** IERC20Metadata(token0Price? token0 : token1).decimals()) / 1e18; + if (IERC20(token0Price? token1 : token0).balanceOf(pool) <= amountOutput){ + return 0; + } + + // heuristic check3: ensure the pool [reserve comparison is consistent with the slot0 price comparison], i.e., asset in less amount should be more expensive in AMM pool + bool token0MoreExpensive = _compareUniV3Tokens(token0Price, rate); + bool token0MoreReserved = _compareUniV3TokenReserves(token0, token1, pool); + if (token0MoreExpensive == token0MoreReserved){ + return 0; + } + + return rate; + } + + /// @dev query current price from V3 pool interface(slot0) with given pool & token0 & token1 + /// @dev and indicator if token0 pricing required (token1/token0 e.g., token0 -> token1) + /// @return the price of required token scaled with 1e18 + function _queryUniV3PriceWithSlot(address token0, address token1, address pool, bool token0Price) internal view returns (uint256) { + (uint256 sqrtPriceX96,,,,,,) = IUniswapV3Pool(pool).slot0(); + uint256 rate; + if (token0Price) { + rate = (((10 ** IERC20Metadata(token0).decimals() * sqrtPriceX96 >> 96) * sqrtPriceX96) >> 96) * 1e18 / 10 ** IERC20Metadata(token1).decimals(); + } else { + rate = ((10 ** IERC20Metadata(token1).decimals() << 192) / sqrtPriceX96 / sqrtPriceX96) * 1e18 / 10 ** IERC20Metadata(token0).decimals(); + } + return rate; + } + + /// @dev check if token0 is more expensive than token1 given slot0 price & if token0 pricing required + function _compareUniV3Tokens(bool token0Price, uint256 rate) internal view returns (bool) { + return token0Price? (rate > 1e18) : (rate < 1e18); + } + + /// @dev check if token0 reserve is bigger than token1 reserve + function _compareUniV3TokenReserves(address token0, address token1, address pool) internal view returns (bool) { + uint256 token0Num = IERC20(token0).balanceOf(pool) / (10 ** IERC20Metadata(token0).decimals()); + uint256 token1Num = IERC20(token1).balanceOf(pool) / (10 ** IERC20Metadata(token1).decimals()); + return token0Num > token1Num; + } + + /// @dev query with the address of the token0 & token1 & the fee tier + /// @return the uniswap v3 pool address + function _getUniV3PoolAddress(address token0, address token1, uint24 fee) internal pure returns (address) { + bytes32 addr = keccak256(abi.encodePacked(hex"ff", UNIV3_FACTORY, keccak256(abi.encode(token0, token1, fee)), UNIV3_POOL_INIT_CODE_HASH)); + return address(uint160(uint256(addr))); + } + + /// === BALANCER === /// + + /// @dev Given the input/output token, returns the quote for input amount from Balancer V2 + function getBalancerPrice(address tokenIn, uint256 amountIn, address tokenOut) public returns (uint256) { + bytes32 poolId = getBalancerV2Pool(tokenIn, tokenOut); + if (poolId == BALANCERV2_NONEXIST_POOLID){ + return 0; + } + + address[] memory assets = new address[](2); + assets[0] = tokenIn; + assets[1] = tokenOut; + + BatchSwapStep[] memory swaps = new BatchSwapStep[](1); + swaps[0] = BatchSwapStep(poolId, 0, 1, amountIn, ""); + + FundManagement memory funds = FundManagement(address(this), false, address(this), false); + + int256[] memory assetDeltas = IBalancerV2Vault(BALANCERV2_VAULT).queryBatchSwap(SwapKind.GIVEN_IN, swaps, assets, funds); + + // asset deltas: either transferring assets from the sender (for positive deltas) or to the recipient (for negative deltas). + return assetDeltas.length > 0 ? uint256(0 - assetDeltas[assetDeltas.length - 1]) : 0; + } + + /// @dev Given the input/output/connector token, returns the quote for input amount from Balancer V2 + function getBalancerPriceWithConnector(address tokenIn, uint256 amountIn, address tokenOut, address connectorToken) public returns (uint256) { + bytes32 firstPoolId = getBalancerV2Pool(tokenIn, connectorToken); + if (firstPoolId == BALANCERV2_NONEXIST_POOLID){ + return 0; + } + bytes32 secondPoolId = getBalancerV2Pool(connectorToken, tokenOut); + if (secondPoolId == BALANCERV2_NONEXIST_POOLID){ + return 0; + } + + address[] memory assets = new address[](3); + assets[0] = tokenIn; + assets[1] = connectorToken; + assets[2] = tokenOut; + + BatchSwapStep[] memory swaps = new BatchSwapStep[](2); + swaps[0] = BatchSwapStep(firstPoolId, 0, 1, amountIn, ""); + swaps[1] = BatchSwapStep(secondPoolId, 1, 2, 0, "");// amount == 0 means use all from previous step + + FundManagement memory funds = FundManagement(address(this), false, address(this), false); + + int256[] memory assetDeltas = IBalancerV2Vault(BALANCERV2_VAULT).queryBatchSwap(SwapKind.GIVEN_IN, swaps, assets, funds); + + // asset deltas: either transferring assets from the sender (for positive deltas) or to the recipient (for negative deltas). + return assetDeltas.length > 0 ? uint256(0 - assetDeltas[assetDeltas.length - 1]) : 0; + } + + /// @return selected BalancerV2 pool given the tokenIn and tokenOut + function getBalancerV2Pool(address tokenIn, address tokenOut) public view returns(bytes32){ + if ((tokenIn == WETH && tokenOut == CREAM) || (tokenOut == WETH && tokenIn == CREAM)){ + return BALANCERV2_CREAM_WETH_POOLID; + } else if ((tokenIn == WETH && tokenOut == GNO) || (tokenOut == WETH && tokenIn == GNO)){ + return BALANCERV2_GNO_WETH_POOLID; + } else if ((tokenIn == WBTC && tokenOut == BADGER) || (tokenOut == WBTC && tokenIn == BADGER)){ + return BALANCERV2_BADGER_WBTC_POOLID; + } else if ((tokenIn == WETH && tokenOut == FEI) || (tokenOut == WETH && tokenIn == FEI)){ + return BALANCERV2_FEI_WETH_POOLID; + } else if ((tokenIn == WETH && tokenOut == BAL) || (tokenOut == WETH && tokenIn == BAL)){ + return BALANCERV2_BAL_WETH_POOLID; + } else if ((tokenIn == WETH && tokenOut == USDC) || (tokenOut == WETH && tokenIn == USDC)){ + return BALANCERV2_USDC_WETH_POOLID; + } else if ((tokenIn == WETH && tokenOut == WBTC) || (tokenOut == WETH && tokenIn == WBTC)){ + return BALANCERV2_WBTC_WETH_POOLID; + } else if ((tokenIn == WETH && tokenOut == WSTETH) || (tokenOut == WETH && tokenIn == WSTETH)){ + return BALANCERV2_WSTETH_WETH_POOLID; + } else if ((tokenIn == WETH && tokenOut == LDO) || (tokenOut == WETH && tokenIn == LDO)){ + return BALANCERV2_LDO_WETH_POOLID; + } else if ((tokenIn == WETH && tokenOut == SRM) || (tokenOut == WETH && tokenIn == SRM)){ + return BALANCERV2_SRM_WETH_POOLID; + } else if ((tokenIn == WETH && tokenOut == rETH) || (tokenOut == WETH && tokenIn == rETH)){ + return BALANCERV2_rETH_WETH_POOLID; + } else if ((tokenIn == WETH && tokenOut == AKITA) || (tokenOut == WETH && tokenIn == AKITA)){ + return BALANCERV2_AKITA_WETH_POOLID; + } else if ((tokenIn == WETH && tokenOut == OHM) || (tokenOut == WETH && tokenIn == OHM) || (tokenIn == DAI && tokenOut == OHM) || (tokenOut == DAI && tokenIn == OHM)){ + return BALANCERV2_OHM_DAI_WETH_POOLID; + } else if ((tokenIn == COW && tokenOut == GNO) || (tokenOut == COW && tokenIn == GNO)){ + return BALANCERV2_COW_GNO_POOLID; + } else if ((tokenIn == WETH && tokenOut == COW) || (tokenOut == WETH && tokenIn == COW)){ + return BALANCERV2_COW_WETH_POOLID; + } else if ((tokenIn == WETH && tokenOut == AURA) || (tokenOut == WETH && tokenIn == AURA)){ + return BALANCERV2_AURA_WETH_POOLID; + } else if ((tokenIn == BALWETHBPT && tokenOut == AURABAL) || (tokenOut == BALWETHBPT && tokenIn == AURABAL)){ + return BALANCERV2_AURABAL_BALWETH_POOLID; + // TODO CHANGE + } else if ((tokenIn == WETH && tokenOut == AURABAL) || (tokenOut == WETH && tokenIn == AURABAL)){ + return BALANCERV2_AURABAL_GRAVIAURA_BALWETH_POOLID; + } else if ((tokenIn == WETH && tokenOut == GRAVIAURA) || (tokenOut == WETH && tokenIn == GRAVIAURA)){ + return BALANCERV2_AURABAL_GRAVIAURA_BALWETH_POOLID; + } else{ + return BALANCERV2_NONEXIST_POOLID; + } + } + + /// === CURVE === /// + + /// @dev Given the address of the CurveLike Router, the input amount, and the path, returns the quote for it + function getCurvePrice(address router, address tokenIn, address tokenOut, uint256 amountIn) public view returns (address, uint256) { + (address pool, uint256 curveQuote) = ICurveRouter(router).get_best_rate(tokenIn, tokenOut, amountIn); + + return (pool, curveQuote); + } + + /// @return assembled curve pools and fees in required Quote struct for given pool + // TODO: Decide if we need fees, as it costs more gas to compute + function _getCurveFees(address _pool) internal view returns (bytes32[] memory, uint256[] memory){ + bytes32[] memory curvePools = new bytes32[](1); + curvePools[0] = convertToBytes32(_pool); + uint256[] memory curvePoolFees = new uint256[](1); + curvePoolFees[0] = ICurvePool(_pool).fee() * CURVE_FEE_SCALE / 1e10;//https://curve.readthedocs.io/factory-pools.html?highlight=fee#StableSwap.fee + return (curvePools, curvePoolFees); + } + + /// === UTILS === /// + + /// @dev Given a address input, return the bytes32 representation + // TODO: Figure out if abi.encode is better + function convertToBytes32(address _input) public pure returns (bytes32){ + return bytes32(uint256(uint160(_input)) << 96); + } +} \ No newline at end of file From ba786298e8b8902bccddf383a654497e697c5874 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Thu, 21 Jul 2022 02:18:49 +0200 Subject: [PATCH 14/53] feat: some notes --- contracts/OnChainPricingMainnet.sol | 49 ++++++++++++++++++----------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/contracts/OnChainPricingMainnet.sol b/contracts/OnChainPricingMainnet.sol index 2300e02..221c5f0 100644 --- a/contracts/OnChainPricingMainnet.sol +++ b/contracts/OnChainPricingMainnet.sol @@ -297,25 +297,33 @@ contract OnChainPricingMainnet { uint24 _maxInRangeFee; { - uint24 _bestFee = _useSinglePoolInUniV3(tokenIn, tokenOut); - for (uint256 i = 0; i < feeTypes;){ - uint24 _fee = univ3_fees[i]; - - // skip othter pools if there is a chosen best pool to go - if (_bestFee > 0 && _fee != _bestFee){ - unchecked { ++i; } - continue; - } - - { - (bool _crossTick, uint256 _outAmt) = _checkSimulationInUniV3(tokenIn, tokenOut, amountIn, _fee); - if (_outAmt > _maxInRangeQuote){ - _maxInRangeQuote = _outAmt; - _maxInRangeFee = _fee; + // Heuristic: If we already know high TVL Pools, use those + uint24 _bestFee = _useSinglePoolInUniV3(tokenIn, tokenOut); + // TODO: Can rewrite to skip the loop entire when `_bestFee` + for (uint256 i = 0; i < feeTypes;){ + uint24 _fee = univ3_fees[i]; + + // skip othter pools if there is a chosen best pool to go + if (_bestFee > 0 && _fee != _bestFee){ + unchecked { ++i; } + continue; } - unchecked { ++i; } - } - } + + { + // TODO: Partial rewrite to perform initial comparison against all simulations based on "liquidity in range" + // If liq is in range, then lowest fee auto-wins + // Else go down fee range with liq in range + // NOTE: A tick is like a ratio, so technically X ticks can offset a fee + // Meaning we prob don't need full quote in majority of cases, but can compare number of ticks + // per pool per fee and pre-rank based on that + (bool _crossTick, uint256 _outAmt) = _checkSimulationInUniV3(tokenIn, tokenOut, amountIn, _fee); + if (_outAmt > _maxInRangeQuote){ + _maxInRangeQuote = _outAmt; + _maxInRangeFee = _fee; + } + unchecked { ++i; } + } + } } return (_maxInRangeQuote, _maxInRangeFee); @@ -359,10 +367,12 @@ contract OnChainPricingMainnet { } } - /// @dev internal function to avoid stack too deap for 1) check in-range liquidity in Uniswap V3 pool 2) full cross-ticks simulation in Uniswap V3 + /// @dev internal function to avoid stack too deep for 1) check in-range liquidity in Uniswap V3 pool 2) full cross-ticks simulation in Uniswap V3 function _checkSimulationInUniV3(address tokenIn, address tokenOut, uint256 amountIn, uint24 _fee) internal view returns (bool, uint256) { bool _crossTick; uint256 _outAmt; + // TODO: Both `checkUniV3InRangeLiquidity` and `simulateUniV3Swap` recompute pool address + // Refactor to only compute once and pass { // in-range swap check: find out whether the swap within current liquidity would move the price across next tick (bool _outOfInRange, uint256 _outputAmount) = checkUniV3InRangeLiquidity(tokenIn, tokenOut, amountIn, _fee); @@ -641,6 +651,7 @@ contract OnChainPricingMainnet { /// @return selected BalancerV2 pool given the tokenIn and tokenOut function getBalancerV2Pool(address tokenIn, address tokenOut) public view returns(bytes32){ + // TODO: Sort tokens so we can refactor to one check instead of two if ((tokenIn == WETH && tokenOut == CREAM) || (tokenOut == WETH && tokenIn == CREAM)){ return BALANCERV2_CREAM_WETH_POOLID; } else if ((tokenIn == WETH && tokenOut == GNO) || (tokenOut == WETH && tokenIn == GNO)){ From e051fc86c4c80b7214d4a54b4a4c3e37082b6341 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Thu, 21 Jul 2022 02:39:21 +0200 Subject: [PATCH 15/53] feat: more thoughts about skipping with UniV3 --- contracts/OnChainPricingMainnet.sol | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contracts/OnChainPricingMainnet.sol b/contracts/OnChainPricingMainnet.sol index 221c5f0..03fa678 100644 --- a/contracts/OnChainPricingMainnet.sol +++ b/contracts/OnChainPricingMainnet.sol @@ -400,6 +400,10 @@ contract OnChainPricingMainnet { } { + // TODO: In a later check, we check slot0 liquidity + // Is there any change that slot0 gives us more information about the liquidity in range, + // Such that optimistically it would immediately allow us to determine a winning pool? + // Prob winning pool would be: Lowest Fee, with Liquidity covered within the tick uint256 _t0Balance = IERC20(_token0).balanceOf(_pool); uint256 _t1Balance = IERC20(_token1).balanceOf(_pool); From 6afc562cd25e74055b777af0bae2a86cedce9aa9 Mon Sep 17 00:00:00 2001 From: rayeaster Date: Thu, 21 Jul 2022 21:10:31 +0800 Subject: [PATCH 16/53] add stable-pool math in balancer --- contracts/BalancerSwapSimulator.sol | 105 +++++++++++++++- contracts/OnChainPricingMainnet.sol | 43 +++++-- contracts/OnChainPricingMainnetLenient.sol | 2 +- .../libraries/balancer/BalancerFixedPoint.sol | 6 + contracts/libraries/balancer/BalancerMath.sol | 32 +++++ .../libraries/balancer/BalancerStableMath.sol | 113 ++++++++++++++++++ interfaces/balancer/IBalancerV2Simulator.sol | 16 ++- interfaces/balancer/IBalancerV2StablePool.sol | 8 ++ .../balancer/IBalancerV2WeightedPool.sol | 1 + tests/gas_benchmark/benchmark_pricer_gas.py | 6 +- tests/on_chain_pricer/test_balancer_pricer.py | 32 +++-- .../test_bribe_tokens_supported.py | 2 +- .../test_swap_exec_on_chain.py | 8 +- tests/on_chain_pricer/test_univ3_pricer.py | 4 +- 14 files changed, 343 insertions(+), 35 deletions(-) create mode 100644 contracts/libraries/balancer/BalancerMath.sol create mode 100644 contracts/libraries/balancer/BalancerStableMath.sol create mode 100644 interfaces/balancer/IBalancerV2StablePool.sol diff --git a/contracts/BalancerSwapSimulator.sol b/contracts/BalancerSwapSimulator.sol index 845f2e9..c6792f8 100644 --- a/contracts/BalancerSwapSimulator.sol +++ b/contracts/BalancerSwapSimulator.sol @@ -3,21 +3,39 @@ pragma solidity 0.7.6; pragma abicoder v2; import "./libraries/balancer/BalancerFixedPoint.sol"; +import "./libraries/balancer/BalancerStableMath.sol"; struct ExactInQueryParam{ + address tokenIn; + address tokenOut; uint256 balanceIn; uint256 weightIn; uint256 balanceOut; uint256 weightOut; uint256 amountIn; + uint256 swapFeePercentage; +} + +struct ExactInStableQueryParam{ + address[] tokens; + uint256[] balances; + uint256 currentAmp; + uint256 tokenIndexIn; + uint256 tokenIndexOut; + uint256 amountIn; + uint256 swapFeePercentage; +} + +interface IERC20Metadata { + function decimals() external view returns (uint8); } /// @dev Swap Simulator for Balancer V2 contract BalancerSwapSimulator { - uint256 internal constant _MAX_IN_RATIO = 0.3e18; + uint256 internal constant _MAX_IN_RATIO = 0.3e18; /// @dev reference https://github.com/balancer-labs/balancer-v2-monorepo/blob/master/pkg/pool-weighted/contracts/WeightedMath.sol#L78 - function calcOutGivenIn(ExactInQueryParam memory _query) public pure returns (uint256) { + function calcOutGivenIn(ExactInQueryParam memory _query) public view returns (uint256) { /********************************************************************************************** // outGivenIn // // aO = amountOut // @@ -27,13 +45,92 @@ contract BalancerSwapSimulator { // wI = weightIn \ \ ( bI + aI ) / / // // wO = weightOut // **********************************************************************************************/ - require(_query.amountIn <= BalancerFixedPoint.mulDown(_query.balanceIn, _MAX_IN_RATIO), '!maxIn'); + + // upscale all balances and amounts + _query.amountIn = _subtractSwapFeeAmount(_query.amountIn, _query.swapFeePercentage); + + uint256 _scalingFactorIn = _computeScalingFactorWeightedPool(_query.tokenIn); + _query.amountIn = BalancerMath.mul(_query.amountIn, _scalingFactorIn); + _query.balanceIn = BalancerMath.mul(_query.balanceIn, _scalingFactorIn); + require(_query.balanceIn > _query.amountIn, '!amtIn'); + + uint256 _scalingFactorOut = _computeScalingFactorWeightedPool(_query.tokenOut); + _query.balanceOut = BalancerMath.mul(_query.balanceOut, _scalingFactorOut); + + require(_query.amountIn <= BalancerFixedPoint.mulDown(_query.balanceIn, _MAX_IN_RATIO), '!maxIn'); + uint256 denominator = BalancerFixedPoint.add(_query.balanceIn, _query.amountIn); uint256 base = BalancerFixedPoint.divUp(_query.balanceIn, denominator); uint256 exponent = BalancerFixedPoint.divDown(_query.weightIn, _query.weightOut); uint256 power = BalancerFixedPoint.powUp(base, exponent); - return BalancerFixedPoint.mulDown(_query.balanceOut, BalancerFixedPoint.complement(power)); + uint256 _scaledOut = BalancerFixedPoint.mulDown(_query.balanceOut, BalancerFixedPoint.complement(power)); + return BalancerMath.divDown(_scaledOut, _scalingFactorOut); + } + + /// @dev reference https://etherscan.io/address/0x7b50775383d3d6f0215a8f290f2c9e2eebbeceb2#code#F1#L244 + function calcOutGivenInForStable(ExactInStableQueryParam memory _query) public view returns (uint256) { + /************************************************************************************************************** + // outGivenIn token x for y - polynomial equation to solve // + // ay = amount out to calculate // + // by = balance token out // + // y = by - ay (finalBalanceOut) // + // D = invariant D D^(n+1) // + // A = amplification coefficient y^2 + ( S - ---------- - D) * y - ------------- = 0 // + // n = number of tokens (A * n^n) A * n^2n * P // + // S = sum of final balances but y // + // P = product of final balances but y // + **************************************************************************************************************/ + + // upscale all balances and amounts + uint256 _tkLen = _query.tokens.length; + uint256[] memory _scalingFactors = new uint256[](_tkLen); + for (uint256 i = 0;i < _tkLen;++i){ + _scalingFactors[i] = _computeScalingFactor(_query.tokens[i]); + } + + _query.amountIn = _subtractSwapFeeAmount(_query.amountIn, _query.swapFeePercentage); + _query.balances = _upscaleStableArray(_query.balances, _scalingFactors); + _query.amountIn = _upscaleStable(_query.amountIn, _scalingFactors[_query.tokenIndexIn]); + + uint256 invariant = BalancerStableMath._calculateInvariant(_query.currentAmp, _query.balances, true); + + _query.balances[_query.tokenIndexIn] = BalancerFixedPoint.add(_query.balances[_query.tokenIndexIn], _query.amountIn); + uint256 finalBalanceOut = BalancerStableMath._getTokenBalanceGivenInvariantAndAllOtherBalances(_query.currentAmp, _query.balances, invariant, _query.tokenIndexOut); + + uint256 _scaledOut = BalancerFixedPoint.sub(_query.balances[_query.tokenIndexOut], BalancerFixedPoint.add(finalBalanceOut, 1)); + return _downscaleStable(_scaledOut, _scalingFactors[_query.tokenIndexOut]); } + + /// @dev scaling factors for weighted pool: reference https://etherscan.io/address/0xc45d42f801105e861e86658648e3678ad7aa70f9#code#F24#L474 + function _computeScalingFactorWeightedPool(address token) private view returns (uint256) { + return 10**BalancerFixedPoint.sub(18, IERC20Metadata(token).decimals()); + } + + /// @dev scaling factors for stable pool: reference https://etherscan.io/address/0x06df3b2bbb68adc8b0e302443692037ed9f91b42#code#F12#L510 + function _computeScalingFactor(address token) internal view returns (uint256) { + return BalancerFixedPoint.ONE * 10**BalancerFixedPoint.sub(18, IERC20Metadata(token).decimals()); + } + + function _upscaleStableArray(uint256[] memory amounts, uint256[] memory scalingFactors) internal pure returns (uint256[] memory) { + uint256 _len = amounts.length; + for (uint256 i = 0; i < _len;++i) { + amounts[i] = _upscaleStable(amounts[i], scalingFactors[i]); + } + return amounts; + } + + function _upscaleStable(uint256 amount, uint256 scalingFactor) internal pure returns (uint256) { + return BalancerFixedPoint.mulDown(amount, scalingFactor); + } + + function _downscaleStable(uint256 amount, uint256 scalingFactor) internal pure returns (uint256) { + return BalancerFixedPoint.divDown(amount, scalingFactor); + } + + function _subtractSwapFeeAmount(uint256 amount, uint256 _swapFeePercentage) public view returns (uint256) { + uint256 feeAmount = BalancerFixedPoint.mulUp(amount, _swapFeePercentage); + return BalancerFixedPoint.sub(amount, feeAmount); + } } \ No newline at end of file diff --git a/contracts/OnChainPricingMainnet.sol b/contracts/OnChainPricingMainnet.sol index 03fa678..691e4d1 100644 --- a/contracts/OnChainPricingMainnet.sol +++ b/contracts/OnChainPricingMainnet.sol @@ -12,6 +12,7 @@ import "../interfaces/uniswap/IV3Pool.sol"; import "../interfaces/uniswap/IV3Quoter.sol"; import "../interfaces/balancer/IBalancerV2Vault.sol"; import "../interfaces/balancer/IBalancerV2WeightedPool.sol"; +import "../interfaces/balancer/IBalancerV2StablePool.sol"; import "../interfaces/curve/ICurveRouter.sol"; import "../interfaces/curve/ICurvePool.sol"; import "../interfaces/uniswap/IV3Simulator.sol"; @@ -104,7 +105,7 @@ contract OnChainPricingMainnet { address public constant GRAVIAURA = 0xBA485b556399123261a5F9c95d413B4f93107407; bytes32 public constant BALANCERV2_AURABAL_GRAVIAURA_BALWETH_POOLID = 0x0578292cb20a443ba1cde459c985ce14ca2bdee5000100000000000000000269; - + bytes32 public constant BALANCERV2_DAI_USDC_USDT_POOLID = 0x06df3b2bbb68adc8b0e302443692037ed9f91b42000000000000000000000063;// Not used due to possible migration: https://forum.balancer.fi/t/vulnerability-disclosure/3179 address public constant AURABAL = 0x616e8BfA43F920657B3497DBf40D6b1A02D4608d; address public constant BALWETHBPT = 0x5c6Ee304399DBdB9C8Ef030aB642B10820DB8F56; @@ -142,7 +143,7 @@ contract OnChainPricingMainnet { /// @dev Given tokenIn, out and amountIn, returns true if a quote will be non-zero /// @notice Doesn't guarantee optimality, just non-zero - function isPairSupported(address tokenIn, address tokenOut, uint256 amountIn) external returns (bool) { + function isPairSupported(address tokenIn, address tokenOut, uint256 amountIn) external view returns (bool) { // Sorted by "assumed" reverse worst case // Go for higher gas cost checks assuming they are offering best precision / good price @@ -175,12 +176,12 @@ contract OnChainPricingMainnet { } /// @dev External function, virtual so you can override, see Lenient Version - function findOptimalSwap(address tokenIn, address tokenOut, uint256 amountIn) external virtual view returns (Quote memory) { + function findOptimalSwap(address tokenIn, address tokenOut, uint256 amountIn) external virtual returns (Quote memory) { return _findOptimalSwap(tokenIn, tokenOut, amountIn); } /// @dev View function for testing the routing of the strategy - function _findOptimalSwap(address tokenIn, address tokenOut, uint256 amountIn) internal view returns (Quote memory) { + function _findOptimalSwap(address tokenIn, address tokenOut, uint256 amountIn) internal returns (Quote memory) { bool wethInvolved = (tokenIn == WETH || tokenOut == WETH); uint256 length = wethInvolved? 5 : 7; // Add length you need @@ -557,6 +558,10 @@ contract OnChainPricingMainnet { if (poolId == BALANCERV2_NONEXIST_POOLID){ return 0; } + return getBalancerPriceWithinPool(poolId, tokenIn, amountIn, tokenOut); + } + + function getBalancerPriceWithinPool(bytes32 poolId, address tokenIn, uint256 amountIn, address tokenOut) public returns (uint256) { address[] memory assets = new address[](2); assets[0] = tokenIn; @@ -579,14 +584,15 @@ contract OnChainPricingMainnet { if (poolId == BALANCERV2_NONEXIST_POOLID){ return 0; } - - address _pool = getAddressFromBytes32Msb(poolId); - uint256 _quote; + return getBalancerQuoteWithinPoolAnalytcially(poolId, tokenIn, amountIn, tokenOut); + } + + function getBalancerQuoteWithinPoolAnalytcially(bytes32 poolId, address tokenIn, uint256 amountIn, address tokenOut) public view returns (uint256) { + uint256 _quote; + address _pool = getAddressFromBytes32Msb(poolId); { - uint256[] memory _weights = IBalancerV2WeightedPool(_pool).getNormalizedWeights(); (address[] memory tokens, uint256[] memory balances, ) = IBalancerV2Vault(BALANCERV2_VAULT).getPoolTokens(poolId); - require(_weights.length == tokens.length, '!lenBAL'); uint256 _inTokenIdx = _findTokenInBalancePool(tokenIn, tokens); require(_inTokenIdx < tokens.length, '!inBAL'); @@ -594,10 +600,23 @@ contract OnChainPricingMainnet { require(_outTokenIdx < tokens.length, '!outBAL'); /// Balancer math for spot price of tokenIn -> tokenOut: weighted value(number * price) relation should be kept - ExactInQueryParam memory _query = ExactInQueryParam(balances[_inTokenIdx], _weights[_inTokenIdx], balances[_outTokenIdx], _weights[_outTokenIdx], amountIn); - _quote = IBalancerV2Simulator(balancerV2Simulator).calcOutGivenIn(_query); + try IBalancerV2StablePool(_pool).getAmplificationParameter() returns (uint256 currentAmp, bool isUpdating, uint256 precision) { + // stable pool math + { + ExactInStableQueryParam memory _stableQuery = ExactInStableQueryParam(tokens, balances, currentAmp, _inTokenIdx, _outTokenIdx, amountIn, IBalancerV2StablePool(_pool).getSwapFeePercentage()); + _quote = IBalancerV2Simulator(balancerV2Simulator).calcOutGivenInForStable(_stableQuery); + } + } catch (bytes memory) { + // weighted pool math + { + uint256[] memory _weights = IBalancerV2WeightedPool(_pool).getNormalizedWeights(); + require(_weights.length == tokens.length, '!lenBAL'); + ExactInQueryParam memory _query = ExactInQueryParam(tokenIn, tokenOut, balances[_inTokenIdx], _weights[_inTokenIdx], balances[_outTokenIdx], _weights[_outTokenIdx], amountIn, IBalancerV2WeightedPool(_pool).getSwapFeePercentage()); + _quote = IBalancerV2Simulator(balancerV2Simulator).calcOutGivenIn(_query); + } + } } - + return _quote; } diff --git a/contracts/OnChainPricingMainnetLenient.sol b/contracts/OnChainPricingMainnetLenient.sol index 362be14..da63abe 100644 --- a/contracts/OnChainPricingMainnetLenient.sol +++ b/contracts/OnChainPricingMainnetLenient.sol @@ -45,7 +45,7 @@ contract OnChainPricingMainnetLenient is OnChainPricingMainnet { // === PRICING === // /// @dev View function for testing the routing of the strategy - function findOptimalSwap(address tokenIn, address tokenOut, uint256 amountIn) external view override returns (Quote memory q) { + function findOptimalSwap(address tokenIn, address tokenOut, uint256 amountIn) external override returns (Quote memory q) { q = _findOptimalSwap(tokenIn, tokenOut, amountIn); q.amountOut = q.amountOut * (MAX_BPS - slippage) / MAX_BPS; } diff --git a/contracts/libraries/balancer/BalancerFixedPoint.sol b/contracts/libraries/balancer/BalancerFixedPoint.sol index e08743f..108c75b 100644 --- a/contracts/libraries/balancer/BalancerFixedPoint.sol +++ b/contracts/libraries/balancer/BalancerFixedPoint.sol @@ -17,6 +17,12 @@ library BalancerFixedPoint { return c; } + function sub(uint256 a, uint256 b) internal pure returns (uint256) { + require(b <= a, '!sub'); + uint256 c = a - b; + return c; + } + function divUp(uint256 a, uint256 b) internal pure returns (uint256) { require(b != 0, '!b0'); diff --git a/contracts/libraries/balancer/BalancerMath.sol b/contracts/libraries/balancer/BalancerMath.sol new file mode 100644 index 0000000..3ee59ae --- /dev/null +++ b/contracts/libraries/balancer/BalancerMath.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.7.6; +pragma abicoder v2; + +// https://github.com/balancer-labs/balancer-v2-monorepo/blob/master/pkg/solidity-utils/contracts/math/Math.sol +library BalancerMath { + + function div(uint256 a, uint256 b, bool roundUp) internal pure returns (uint256) { + return roundUp ? divUp(a, b) : divDown(a, b); + } + + function mul(uint256 a, uint256 b) internal pure returns (uint256) { + uint256 c = a * b; + require(a == 0 || c / a == b, '!OVEF'); + return c; + } + + function divDown(uint256 a, uint256 b) internal pure returns (uint256) { + require(b != 0, '!b0'); + return a / b; + } + + function divUp(uint256 a, uint256 b) internal pure returns (uint256) { + require(b != 0, '!b0'); + + if (a == 0) { + return 0; + } else { + return 1 + (a - 1) / b; + } + } +} \ No newline at end of file diff --git a/contracts/libraries/balancer/BalancerStableMath.sol b/contracts/libraries/balancer/BalancerStableMath.sol new file mode 100644 index 0000000..65d0c92 --- /dev/null +++ b/contracts/libraries/balancer/BalancerStableMath.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.7.6; +pragma abicoder v2; + +import "./BalancerMath.sol"; +import "./BalancerFixedPoint.sol"; + +// https://etherscan.io/address/0x7b50775383d3d6f0215a8f290f2c9e2eebbeceb2#code#F14#L25 +library BalancerStableMath { + using BalancerFixedPoint for uint256; + + uint256 internal constant _AMP_PRECISION = 1e3; + + function _calculateInvariant(uint256 amplificationParameter, uint256[] memory balances, bool roundUp) internal pure returns (uint256) { + /********************************************************************************************** + // invariant // + // D = invariant D^(n+1) // + // A = amplification coefficient A n^n S + D = A D n^n + ----------- // + // S = sum of balances n^n P // + // P = product of balances // + // n = number of tokens // + **********************************************************************************************/ + + uint256 sum = 0; // S in the Curve version + uint256 numTokens = balances.length; + for (uint256 i = 0; i < numTokens; i++) { + sum = sum.add(balances[i]); + } + if (sum == 0) { + return 0; + } + + uint256 prevInvariant = 0; + uint256 invariant = sum; + uint256 ampTimesTotal = amplificationParameter * numTokens; + + for (uint256 i = 0; i < 255; i++) { + uint256 P_D = balances[0] * numTokens; + for (uint256 j = 1; j < numTokens; j++) { + P_D = BalancerMath.div(BalancerMath.mul(BalancerMath.mul(P_D, balances[j]), numTokens), invariant, roundUp); + } + prevInvariant = invariant; + invariant = BalancerMath.div( + BalancerMath.mul(BalancerMath.mul(numTokens, invariant), invariant).add( + BalancerMath.div(BalancerMath.mul(BalancerMath.mul(ampTimesTotal, sum), P_D), _AMP_PRECISION, roundUp) + ), + BalancerMath.mul(numTokens + 1, invariant).add( + // No need to use checked arithmetic for the amp precision, the amp is guaranteed to be at least 1 + BalancerMath.div(BalancerMath.mul(ampTimesTotal - _AMP_PRECISION, P_D), _AMP_PRECISION, !roundUp) + ), + roundUp + ); + + if (invariant > prevInvariant) { + if (invariant - prevInvariant <= 1) { + return invariant; + } + } else if (prevInvariant - invariant <= 1) { + return invariant; + } + } + + require(invariant < 0, '!INVT'); + } + + function _getTokenBalanceGivenInvariantAndAllOtherBalances(uint256 amplificationParameter, uint256[] memory balances, uint256 invariant, uint256 tokenIndex) internal pure returns (uint256) { + // Rounds result up overall + + uint256 ampTimesTotal = amplificationParameter * balances.length; + uint256 sum = balances[0]; + uint256 P_D = balances[0] * balances.length; + for (uint256 j = 1; j < balances.length; j++) { + P_D = BalancerMath.divDown(BalancerMath.mul(BalancerMath.mul(P_D, balances[j]), balances.length), invariant); + sum = sum.add(balances[j]); + } + // No need to use safe math, based on the loop above `sum` is greater than or equal to `balances[tokenIndex]` + sum = sum - balances[tokenIndex]; + + uint256 inv2 = BalancerMath.mul(invariant, invariant); + // We remove the balance from c by multiplying it + uint256 c = BalancerMath.mul( + BalancerMath.mul(BalancerMath.divUp(inv2, BalancerMath.mul(ampTimesTotal, P_D)), _AMP_PRECISION), + balances[tokenIndex] + ); + uint256 b = sum.add(BalancerMath.mul(BalancerMath.divDown(invariant, ampTimesTotal), _AMP_PRECISION)); + + // We iterate to find the balance + uint256 prevTokenBalance = 0; + // We multiply the first iteration outside the loop with the invariant to set the value of the + // initial approximation. + uint256 tokenBalance = BalancerMath.divUp(inv2.add(c), invariant.add(b)); + + for (uint256 i = 0; i < 255; i++) { + prevTokenBalance = tokenBalance; + + tokenBalance = BalancerMath.divUp( + BalancerMath.mul(tokenBalance, tokenBalance).add(c), + BalancerMath.mul(tokenBalance, 2).add(b).sub(invariant) + ); + + if (tokenBalance > prevTokenBalance) { + if (tokenBalance - prevTokenBalance <= 1) { + return tokenBalance; + } + } else if (prevTokenBalance - tokenBalance <= 1) { + return tokenBalance; + } + } + + require(tokenBalance < 0, '!COVG'); + } + +} \ No newline at end of file diff --git a/interfaces/balancer/IBalancerV2Simulator.sol b/interfaces/balancer/IBalancerV2Simulator.sol index 8cc16a5..d8a950a 100644 --- a/interfaces/balancer/IBalancerV2Simulator.sol +++ b/interfaces/balancer/IBalancerV2Simulator.sol @@ -3,13 +3,27 @@ pragma solidity 0.8.10; pragma abicoder v2; struct ExactInQueryParam{ + address tokenIn; + address tokenOut; uint256 balanceIn; uint256 weightIn; uint256 balanceOut; uint256 weightOut; uint256 amountIn; + uint256 swapFeePercentage; +} + +struct ExactInStableQueryParam{ + address[] tokens; + uint256[] balances; + uint256 currentAmp; + uint256 tokenIndexIn; + uint256 tokenIndexOut; + uint256 amountIn; + uint256 swapFeePercentage; } interface IBalancerV2Simulator { - function calcOutGivenIn(ExactInQueryParam memory _query) external pure returns (uint256); + function calcOutGivenIn(ExactInQueryParam memory _query) external view returns (uint256); + function calcOutGivenInForStable(ExactInStableQueryParam memory _query) external view returns (uint256); } \ No newline at end of file diff --git a/interfaces/balancer/IBalancerV2StablePool.sol b/interfaces/balancer/IBalancerV2StablePool.sol new file mode 100644 index 0000000..ec9a3dc --- /dev/null +++ b/interfaces/balancer/IBalancerV2StablePool.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity >=0.7.5; +pragma abicoder v2; + +interface IBalancerV2StablePool { + function getAmplificationParameter() external view returns (uint256 value, bool isUpdating, uint256 precision); + function getSwapFeePercentage() external view returns (uint256); +} diff --git a/interfaces/balancer/IBalancerV2WeightedPool.sol b/interfaces/balancer/IBalancerV2WeightedPool.sol index 24b9a75..6b8573c 100644 --- a/interfaces/balancer/IBalancerV2WeightedPool.sol +++ b/interfaces/balancer/IBalancerV2WeightedPool.sol @@ -4,4 +4,5 @@ pragma abicoder v2; interface IBalancerV2WeightedPool { function getNormalizedWeights() external view returns (uint256[] memory); + function getSwapFeePercentage() external view returns (uint256); } diff --git a/tests/gas_benchmark/benchmark_pricer_gas.py b/tests/gas_benchmark/benchmark_pricer_gas.py index 776062d..720a9c6 100644 --- a/tests/gas_benchmark/benchmark_pricer_gas.py +++ b/tests/gas_benchmark/benchmark_pricer_gas.py @@ -39,7 +39,7 @@ def test_gas_only_balancer_v2(oneE18, weth, aura, pricer): tx = pricer.findOptimalSwap(token, weth.address, sell_amount) assert tx.return_value[0] == 5 ## BALANCER assert tx.return_value[1] > 0 - assert tx.gas_used <= 95000 ## 91345 in test simulation + assert tx.gas_used <= 110000 ## 101190 in test simulation def test_gas_only_balancer_v2_with_weth(oneE18, wbtc, aura, pricer): token = aura # some swap (AURA-WETH-WBTC) only in Balancer V2 via WETH in between as connector @@ -50,7 +50,7 @@ def test_gas_only_balancer_v2_with_weth(oneE18, wbtc, aura, pricer): tx = pricer.findOptimalSwap(token, wbtc.address, sell_amount) assert tx.return_value[0] == 6 ## BALANCERWITHWETH assert tx.return_value[1] > 0 - assert tx.gas_used <= 145000 ## 141062 in test simulation + assert tx.gas_used <= 170000 ## 161690 in test simulation def test_gas_only_uniswap_v3(oneE18, weth, pricer): token = "0xf4d2888d29D722226FafA5d9B24F9164c092421E" # some swap (LOOKS-WETH) only in Uniswap V3 @@ -83,5 +83,5 @@ def test_gas_almost_everything(oneE18, wbtc, weth, pricer): tx = pricer.findOptimalSwap(token, wbtc.address, sell_amount) assert (tx.return_value[0] <= 3 or tx.return_value[0] == 5) ## CURVE or UNIV2 or SUSHI or UNIV3 or BALANCER assert tx.return_value[1] > 0 - assert tx.gas_used <= 190000 ## 183810 in test simulation + assert tx.gas_used <= 210000 ## 200229 in test simulation \ No newline at end of file diff --git a/tests/on_chain_pricer/test_balancer_pricer.py b/tests/on_chain_pricer/test_balancer_pricer.py index 531a09f..de7ac84 100644 --- a/tests/on_chain_pricer/test_balancer_pricer.py +++ b/tests/on_chain_pricer/test_balancer_pricer.py @@ -4,7 +4,25 @@ #import sys #from scripts.get_price import get_coingecko_price, get_coinmarketcap_price, get_coinmarketcap_metadata -import pytest +import pytest + +""" + getBalancerPrice quote for token A swapped to token B directly using given balancer pool: A - > B +""" +def test_get_balancer_price_stable_analytical(oneE18, usdc, dai, pricer): + ## 1e18 + sell_count = 50000 + sell_amount = sell_count * oneE18 + + ## minimum quote for DAI in USDC(1e6) + p = sell_count * 0.999 * 1000000 + + ## there is a proper pool in Balancer for DAI in USDC + poolId = pricer.BALANCERV2_DAI_USDC_USDT_POOLID() + q = pricer.getBalancerPriceWithinPool(poolId, dai.address, sell_amount, usdc.address).return_value + assert q >= p + quote = pricer.getBalancerQuoteWithinPoolAnalytcially(poolId, dai.address, sell_amount, usdc.address) + assert quote == q """ getBalancerPrice quote for token A swapped to token B directly using given balancer pool: A - > B @@ -15,7 +33,7 @@ def test_get_balancer_price(oneE18, weth, usdc, pricer): ## minimum quote for ETH in USDC(1e6) p = 1 * 500 * 1000000 - quote = pricer.getBalancerPrice(weth.address, sell_amount, usdc.address) + quote = pricer.getBalancerPrice(weth.address, sell_amount, usdc.address).return_value assert quote >= p ## price sanity check with fine liquidity @@ -32,8 +50,8 @@ def test_get_balancer_price_with_connector(oneE18, wbtc, usdc, weth, pricer): sell_amount = sell_count * 100000000 ## minimum quote for WBTC in USDC(1e6) - p = sell_count * 10000 * 1000000 - quote = pricer.getBalancerPriceWithConnector(wbtc.address, sell_amount, usdc.address, weth.address) + p = sell_count * 15000 * 1000000 + quote = pricer.getBalancerPriceWithConnector(wbtc.address, sell_amount, usdc.address, weth.address).return_value assert quote >= p ## price sanity check with dime liquidity @@ -50,7 +68,7 @@ def test_get_balancer_price_nonexistence(oneE18, cvx, weth, pricer): sell_amount = 100 * oneE18 ## no proper pool in Balancer for WETH in CVX - quote = pricer.getBalancerPrice(weth.address, sell_amount, cvx.address) + quote = pricer.getBalancerPrice(weth.address, sell_amount, cvx.address).return_value assert quote == 0 """ @@ -74,7 +92,7 @@ def test_get_balancer_price_with_connector_analytical(oneE18, wbtc, usdc, weth, sell_amount = sell_count * 100000000 ## minimum quote for WBTC in USDC(1e6) - p = sell_count * 10000 * 1000000 + p = sell_count * 15000 * 1000000 quote = pricer.getBalancerPriceWithConnectorAnalytically(wbtc.address, sell_amount, usdc.address, weth.address) assert quote >= p @@ -104,5 +122,5 @@ def test_get_balancer_price_aurabal_analytical(oneE18, aurabal, weth, pricer): ## there is a proper pool in Balancer for AURABAL in WETH quote = pricer.getBalancerPriceAnalytically(aurabal.address, sell_amount, weth.address) - assert quote >= p + assert quote >= p \ No newline at end of file diff --git a/tests/on_chain_pricer/test_bribe_tokens_supported.py b/tests/on_chain_pricer/test_bribe_tokens_supported.py index d9a9f1e..56a0591 100644 --- a/tests/on_chain_pricer/test_bribe_tokens_supported.py +++ b/tests/on_chain_pricer/test_bribe_tokens_supported.py @@ -69,7 +69,7 @@ def test_bribes_get_optimal_quote(pricer, token): ## 1e18 for everything, even with insane slippage will still return non-zero which is sufficient at this time AMOUNT = 1e18 - res = pricer.findOptimalSwap(token, WETH, AMOUNT) + res = pricer.findOptimalSwap(token, WETH, AMOUNT).return_value assert res[1] > 0 diff --git a/tests/on_chain_pricer/test_swap_exec_on_chain.py b/tests/on_chain_pricer/test_swap_exec_on_chain.py index d643439..526ac30 100644 --- a/tests/on_chain_pricer/test_swap_exec_on_chain.py +++ b/tests/on_chain_pricer/test_swap_exec_on_chain.py @@ -59,7 +59,7 @@ def test_swap_in_univ3_single(oneE18, wbtc_whale, wbtc, usdc, pricer, swapexecut ## minimum quote for WBTC in USDC(1e6) p = 1 * 15000 * 1000000 - quote = pricer.findOptimalSwap(wbtc.address, usdc.address, sell_amount) + quote = pricer.findOptimalSwap(wbtc.address, usdc.address, sell_amount).return_value assert quote[1] >= p ## swap on chain @@ -82,7 +82,7 @@ def test_swap_in_univ3(oneE18, wbtc_whale, wbtc, weth, usdc, pricer, swapexecuto ## minimum quote for WBTC in USDC(1e6) p = 1 * 15000 * 1000000 - quote = pricer.findOptimalSwap(wbtc.address, usdc.address, sell_amount) + quote = pricer.findOptimalSwap(wbtc.address, usdc.address, sell_amount).return_value assert quote[1] >= p ## swap on chain @@ -105,7 +105,7 @@ def test_swap_in_balancer_batch(oneE18, wbtc_whale, wbtc, weth, usdc, pricer, sw ## minimum quote for WBTC in USDC(1e6) p = 1 * 15000 * 1000000 - quote = pricer.findOptimalSwap(wbtc.address, usdc.address, sell_amount) + quote = pricer.findOptimalSwap(wbtc.address, usdc.address, sell_amount).return_value assert quote[1] >= p ## swap on chain @@ -129,7 +129,7 @@ def test_swap_in_balancer_single(oneE18, weth_whale, weth, usdc, pricer, swapexe ## minimum quote for WETH in USDC(1e6) p = 1 * 500 * 1000000 - quote = pricer.findOptimalSwap(weth.address, usdc.address, sell_amount) + quote = pricer.findOptimalSwap(weth.address, usdc.address, sell_amount).return_value assert quote[1] >= p ## swap on chain diff --git a/tests/on_chain_pricer/test_univ3_pricer.py b/tests/on_chain_pricer/test_univ3_pricer.py index 9be1c79..c86fd4f 100644 --- a/tests/on_chain_pricer/test_univ3_pricer.py +++ b/tests/on_chain_pricer/test_univ3_pricer.py @@ -17,7 +17,7 @@ def test_get_univ3_price_in_range(oneE18, weth, usdc, usdc_whale, pricer): assert quote[0] == quoteInRange[1] ## check against quoter - quoterP = interface.IV3Quoter(pricer.UNIV3_QUOTER()).quoteExactInputSingle(weth.address, usdc.address, quote[1], sell_amount, 0, {'from': usdc_whale.address}) + quoterP = interface.IV3Quoter(pricer.UNIV3_QUOTER()).quoteExactInputSingle(weth.address, usdc.address, quote[1], sell_amount, 0, {'from': usdc_whale.address}).return_value assert quoterP == quote[0] ## fee-0.05% pool is the chosen one among (0.05%, 0.3%, 1%)! @@ -39,7 +39,7 @@ def test_get_univ3_price_cross_tick(oneE18, weth, usdc, usdc_whale, pricer): assert quote[0] == quoteCrossTicks ## check against quoter - quoterP = interface.IV3Quoter(pricer.UNIV3_QUOTER()).quoteExactInputSingle(weth.address, usdc.address, quote[1], sell_amount, 0, {'from': usdc_whale.address}) + quoterP = interface.IV3Quoter(pricer.UNIV3_QUOTER()).quoteExactInputSingle(weth.address, usdc.address, quote[1], sell_amount, 0, {'from': usdc_whale.address}).return_value assert (abs(quoterP - quote[0]) / quoterP) <= 0.0015 ## thousandsth in quote diff for a millions-dollar-worth swap ## fee-0.05% pool is the chosen one among (0.05%, 0.3%, 1%)! From 94c8e3ea2aaf9d508bfb6197f76445d53575d71f Mon Sep 17 00:00:00 2001 From: sajanrajdev Date: Thu, 21 Jul 2022 14:14:59 -0400 Subject: [PATCH 17/53] chore: reqs and gitignore --- .gitignore | 1 + requirements.txt | 9 +++++++++ 2 files changed, 10 insertions(+) create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore index 0ff2835..1bb6ebd 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ __pycache__ build/ reports/ .env +venv/ # Node/npm node_modules/ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f085a82 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +black==21.9b0 +eth-brownie>=1.11.0,<2.0.0 +dotmap==1.3.24 +python-dotenv==0.16.0 +tabulate==0.8.9 +rich==10.7.0 +click==8.0.1 +platformdirs==2.3.0 +regex==2021.8.28 \ No newline at end of file From bf801ee5d882e3cc7a784a0297ed2167c85187d6 Mon Sep 17 00:00:00 2001 From: sajanrajdev Date: Thu, 21 Jul 2022 14:16:05 -0400 Subject: [PATCH 18/53] test: pricing equivalncies --- tests/conftest.py | 4 + .../heuristic_equivalency.py | 152 ++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 tests/heuristic_equivalency/heuristic_equivalency.py diff --git a/tests/conftest.py b/tests/conftest.py index c0e0a31..9aa8077 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -44,6 +44,10 @@ def pricer(): balancerV2Simulator = BalancerSwapSimulator.deploy({"from": a[0]}) return OnChainPricingMainnet.deploy(univ3simulator.address, balancerV2Simulator.address, {"from": a[0]}) +@pytest.fixture +def pricer_legacy(): + return FullOnChainPricingMainnet.deploy({"from": a[0]}) + @pytest.fixture def lenient_contract(): ## NOTE: We have 5% slippage on this one diff --git a/tests/heuristic_equivalency/heuristic_equivalency.py b/tests/heuristic_equivalency/heuristic_equivalency.py new file mode 100644 index 0000000..9dbcc34 --- /dev/null +++ b/tests/heuristic_equivalency/heuristic_equivalency.py @@ -0,0 +1,152 @@ +from brownie import chain +from rich.console import Console + +console = Console() + +""" + Evaluates the pricing quotes generated by the optimized (heuristic) version of the OnChainPricingMainnet + in contrast to its legacy version. The new version should lead to the same optimal price while consuming + less gas. + + Tests excluded from main test suite as core functionalities are not tested here. In order to add to test + suite, modify the file name to: `test_heuristic_equivalency.py`. Note that tested routes depend on current + liquidity state and, if liquidity moves between protocols, some assertions may fail. +""" + +### Test findOptimalSwap Equivalencies for different cases + +def test_pricing_equivalency_uniswap_v2(weth, pricer, pricer_legacy): + token = "0xBC7250C8c3eCA1DfC1728620aF835FCa489bFdf3" # some swap (GM-WETH) only in Uniswap V2 + ## 1e18 + sell_count = 100000000 + sell_amount = sell_count * 1000000000 ## 1e9 + + chain.snapshot() # To price under same chain conditions (just because) + tx = pricer.findOptimalSwap(token, weth.address, sell_amount) + assert tx.return_value[0] == 1 ## UNIV2 + quote = tx.return_value[1] + + chain.revert() + tx2 = pricer_legacy.findOptimalSwap(token, weth.address, sell_amount) + assert tx2.return_value[0] == 1 ## UNIV2 + quote_legacy = tx2.return_value[1] + + assert quote >= quote_legacy # Optimized quote must be the same or better + assert tx.gas_used < tx2.gas_used + +def test_pricing_equivalency_uniswap_v2_sushi(oneE18, weth, pricer, pricer_legacy): + token = "0x2e9d63788249371f1DFC918a52f8d799F4a38C94" # some swap (TOKE-WETH) only in Uniswap V2 & SushiSwap + ## 1e18 + sell_count = 5000 + sell_amount = sell_count * oneE18 ## 1e18 + + chain.snapshot() # To price under same chain conditions (just because) + tx = pricer.findOptimalSwap(token, weth.address, sell_amount) + assert (tx.return_value[0] == 1 or tx.return_value[0] == 2) ## UNIV2 or SUSHI + quote = tx.return_value[1] + + chain.revert() + tx2 = pricer_legacy.findOptimalSwap(token, weth.address, sell_amount) + assert (tx2.return_value[0] == 1 or tx2.return_value[0] == 2) ## UNIV2 or SUSHI + quote_legacy = tx2.return_value[1] + + assert quote >= quote_legacy # Optimized quote must be the same or better + assert tx.gas_used < tx2.gas_used + +def test_pricing_equivalency_balancer_v2(oneE18, weth, aura, pricer, pricer_legacy): + token = aura # some swap (AURA-WETH) only in Balancer V2 + ## 1e18 + sell_count = 2000 + sell_amount = sell_count * oneE18 ## 1e18 + + chain.snapshot() # To price under same chain conditions (just because) + tx = pricer.findOptimalSwap(token, weth.address, sell_amount) + assert tx.return_value[0] == 5 ## BALANCER + quote = tx.return_value[1] + + chain.revert() + tx2 = pricer_legacy.findOptimalSwap(token, weth.address, sell_amount) + assert tx2.return_value[0] == 5 ## BALANCER + quote_legacy = tx2.return_value[1] + + assert quote >= quote_legacy # Optimized quote must be the same or better + assert tx.gas_used < tx2.gas_used + +def test_pricing_equivalency_balancer_v2_with_weth(oneE18, wbtc, aura, pricer, pricer_legacy): + token = aura # some swap (AURA-WETH-WBTC) only in Balancer V2 via WETH in between as connector + ## 1e18 + sell_count = 2000 + sell_amount = sell_count * oneE18 ## 1e18 + + chain.snapshot() # To price under same chain conditions (just because) + tx = pricer.findOptimalSwap(token, wbtc.address, sell_amount) + assert tx.return_value[0] == 6 ## BALANCERWITHWETH + quote = tx.return_value[1] + + chain.revert() + tx2 = pricer_legacy.findOptimalSwap(token, wbtc.address, sell_amount) + assert tx2.return_value[0] == 6 ## BALANCERWITHWETH + quote_legacy = tx2.return_value[1] + + assert quote >= quote_legacy # Optimized quote must be the same or better + assert tx.gas_used < tx2.gas_used + +def test_pricing_equivalency_uniswap_v3(oneE18, weth, pricer, pricer_legacy): + token = "0xf4d2888d29D722226FafA5d9B24F9164c092421E" # some swap (LOOKS-WETH) only in Uniswap V3 + ## 1e18 + sell_count = 600000 + sell_amount = sell_count * oneE18 ## 1e18 + + chain.snapshot() # To price under same chain conditions (just because) + tx = pricer.findOptimalSwap(token, weth.address, sell_amount) + assert tx.return_value[0] == 3 ## UNIV3 + quote = tx.return_value[1] + + chain.revert() + tx2 = pricer_legacy.findOptimalSwap(token, weth.address, sell_amount) + assert tx2.return_value[0] == 3 ## UNIV3 + quote_legacy = tx2.return_value[1] + + assert quote >= quote_legacy # Optimized quote must be the same or better + assert tx.gas_used < tx2.gas_used + +def test_pricing_equivalency_uniswap_v3_with_weth(oneE18, wbtc, pricer, pricer_legacy): + token = "0xf4d2888d29D722226FafA5d9B24F9164c092421E" # some swap (LOOKS-WETH-WBTC) only in Uniswap V3 via WETH in between as connector + ## 1e18 + sell_count = 600000 + sell_amount = sell_count * oneE18 ## 1e18 + + chain.snapshot() # To price under same chain conditions (just because) + tx = pricer.findOptimalSwap(token, wbtc.address, sell_amount) + assert tx.return_value[0] == 4 ## UNIV3WITHWETH + quote = tx.return_value[1] + + chain.revert() + tx2 = pricer_legacy.findOptimalSwap(token, wbtc.address, sell_amount) + assert tx2.return_value[0] == 4 ## UNIV3WITHWETH + quote_legacy = tx2.return_value[1] + + assert quote >= quote_legacy # Optimized quote must be the same or better + assert tx.gas_used < tx2.gas_used + +def test_pricing_equivalency_almost_everything(oneE18, wbtc, weth, pricer, pricer_legacy): + token = weth # some swap (WETH-WBTC) almost in every DEX, the most gas-consuming scenario + ## 1e18 + sell_count = 10 + sell_amount = sell_count * oneE18 ## 1e18 + + chain.snapshot() # To price under same chain conditions (just because) + tx = pricer.findOptimalSwap(token, wbtc.address, sell_amount) + assert (tx.return_value[0] <= 3 or tx.return_value[0] == 5) ## CURVE or UNIV2 or SUSHI or UNIV3 or BALANCER + quote = tx.return_value[1] + + chain.revert() + tx2 = pricer_legacy.findOptimalSwap(token, wbtc.address, sell_amount) + assert (tx2.return_value[0] <= 3 or tx2.return_value[0] == 5) ## CURVE or UNIV2 or SUSHI or UNIV3 or BALANCER + quote_legacy = tx2.return_value[1] + + assert quote >= quote_legacy # Optimized quote must be the same or better + assert tx.gas_used < tx2.gas_used + + +### TODO: Test specific pricing functions for different underlying protocols \ No newline at end of file From 664b76d52f592029604306f65ee720cb7b1823b6 Mon Sep 17 00:00:00 2001 From: sajanrajdev Date: Thu, 21 Jul 2022 14:16:19 -0400 Subject: [PATCH 19/53] fix: minor syntax --- tests/gas_benchmark/benchmark_pricer_gas.py | 14 +++++++------- tests/on_chain_pricer/test_swap_exec_on_chain.py | 2 +- tests/on_chain_pricer/test_univ3_pricer_simu.py | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/gas_benchmark/benchmark_pricer_gas.py b/tests/gas_benchmark/benchmark_pricer_gas.py index 720a9c6..d50ae0e 100644 --- a/tests/gas_benchmark/benchmark_pricer_gas.py +++ b/tests/gas_benchmark/benchmark_pricer_gas.py @@ -11,7 +11,7 @@ def test_gas_only_uniswap_v2(oneE18, weth, pricer): token = "0xBC7250C8c3eCA1DfC1728620aF835FCa489bFdf3" # some swap (GM-WETH) only in Uniswap V2 ## 1e18 - sell_count = 100000000; + sell_count = 100000000 sell_amount = sell_count * 1000000000 ## 1e9 tx = pricer.findOptimalSwap(token, weth.address, sell_amount) @@ -22,7 +22,7 @@ def test_gas_only_uniswap_v2(oneE18, weth, pricer): def test_gas_uniswap_v2_sushi(oneE18, weth, pricer): token = "0x2e9d63788249371f1DFC918a52f8d799F4a38C94" # some swap (TOKE-WETH) only in Uniswap V2 & SushiSwap ## 1e18 - sell_count = 5000; + sell_count = 5000 sell_amount = sell_count * oneE18 ## 1e18 tx = pricer.findOptimalSwap(token, weth.address, sell_amount) @@ -33,7 +33,7 @@ def test_gas_uniswap_v2_sushi(oneE18, weth, pricer): def test_gas_only_balancer_v2(oneE18, weth, aura, pricer): token = aura # some swap (AURA-WETH) only in Balancer V2 ## 1e18 - sell_count = 2000; + sell_count = 2000 sell_amount = sell_count * oneE18 ## 1e18 tx = pricer.findOptimalSwap(token, weth.address, sell_amount) @@ -44,7 +44,7 @@ def test_gas_only_balancer_v2(oneE18, weth, aura, pricer): def test_gas_only_balancer_v2_with_weth(oneE18, wbtc, aura, pricer): token = aura # some swap (AURA-WETH-WBTC) only in Balancer V2 via WETH in between as connector ## 1e18 - sell_count = 2000; + sell_count = 2000 sell_amount = sell_count * oneE18 ## 1e18 tx = pricer.findOptimalSwap(token, wbtc.address, sell_amount) @@ -55,7 +55,7 @@ def test_gas_only_balancer_v2_with_weth(oneE18, wbtc, aura, pricer): def test_gas_only_uniswap_v3(oneE18, weth, pricer): token = "0xf4d2888d29D722226FafA5d9B24F9164c092421E" # some swap (LOOKS-WETH) only in Uniswap V3 ## 1e18 - sell_count = 600000; + sell_count = 600000 sell_amount = sell_count * oneE18 ## 1e18 tx = pricer.findOptimalSwap(token, weth.address, sell_amount) @@ -66,7 +66,7 @@ def test_gas_only_uniswap_v3(oneE18, weth, pricer): def test_gas_only_uniswap_v3_with_weth(oneE18, wbtc, pricer): token = "0xf4d2888d29D722226FafA5d9B24F9164c092421E" # some swap (LOOKS-WETH-WBTC) only in Uniswap V3 via WETH in between as connector ## 1e18 - sell_count = 600000; + sell_count = 600000 sell_amount = sell_count * oneE18 ## 1e18 tx = pricer.findOptimalSwap(token, wbtc.address, sell_amount) @@ -77,7 +77,7 @@ def test_gas_only_uniswap_v3_with_weth(oneE18, wbtc, pricer): def test_gas_almost_everything(oneE18, wbtc, weth, pricer): token = weth # some swap (WETH-WBTC) almost in every DEX, the most gas-consuming scenario ## 1e18 - sell_count = 10; + sell_count = 10 sell_amount = sell_count * oneE18 ## 1e18 tx = pricer.findOptimalSwap(token, wbtc.address, sell_amount) diff --git a/tests/on_chain_pricer/test_swap_exec_on_chain.py b/tests/on_chain_pricer/test_swap_exec_on_chain.py index 526ac30..e0838be 100644 --- a/tests/on_chain_pricer/test_swap_exec_on_chain.py +++ b/tests/on_chain_pricer/test_swap_exec_on_chain.py @@ -22,7 +22,7 @@ def test_swap_in_curve(oneE18, weth_whale, weth, crv, pricer, swapexecutor): minOutput = quote[1] * slippageTolerance balBefore = crv.balanceOf(weth_whale) - poolBytes = pricer.convertToBytes32(quote[0]); + poolBytes = pricer.convertToBytes32(quote[0]) swapexecutor.doOptimalSwapWithQuote(weth.address, crv.address, sell_amount, (0, minOutput, [poolBytes], []), {'from': weth_whale}) balAfter = crv.balanceOf(weth_whale) assert (balAfter - balBefore) >= minOutput diff --git a/tests/on_chain_pricer/test_univ3_pricer_simu.py b/tests/on_chain_pricer/test_univ3_pricer_simu.py index a1d833e..fa3de8a 100644 --- a/tests/on_chain_pricer/test_univ3_pricer_simu.py +++ b/tests/on_chain_pricer/test_univ3_pricer_simu.py @@ -7,7 +7,7 @@ """ def test_simu_univ3_swap(oneE18, weth, usdc, pricer): ## 1e18 - sell_count = 10; + sell_count = 10 sell_amount = sell_count * oneE18 ## minimum quote for ETH in USDC(1e6) ## Rip ETH price @@ -21,7 +21,7 @@ def test_simu_univ3_swap(oneE18, weth, usdc, pricer): """ def test_simu_univ3_swap2(oneE18, weth, wbtc, pricer): ## 1e8 - sell_count = 10; + sell_count = 10 sell_amount = sell_count * 100000000 ## minimum quote for BTC in ETH(1e18) ## Rip ETH price From 9e6d0cab56bb05cee677e946731f2cf4b5417c0d Mon Sep 17 00:00:00 2001 From: sajanrajdev Date: Thu, 21 Jul 2022 17:37:34 -0400 Subject: [PATCH 20/53] test: balancer equivalencies --- .../heuristic_equivalency.py | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/tests/heuristic_equivalency/heuristic_equivalency.py b/tests/heuristic_equivalency/heuristic_equivalency.py index 9dbcc34..e020253 100644 --- a/tests/heuristic_equivalency/heuristic_equivalency.py +++ b/tests/heuristic_equivalency/heuristic_equivalency.py @@ -149,4 +149,28 @@ def test_pricing_equivalency_almost_everything(oneE18, wbtc, weth, pricer, price assert tx.gas_used < tx2.gas_used -### TODO: Test specific pricing functions for different underlying protocols \ No newline at end of file +### Test specific pricing functions for different underlying protocols + +def test_balancer_pricing_equivalency(oneE18, weth, usdc, pricer, pricer_legacy): + ## 1e18 + sell_amount = 1 * oneE18 + + quote = pricer.getBalancerPriceAnalytically(weth.address, sell_amount, usdc.address) + quote_legacy = pricer_legacy.getBalancerPrice(weth.address, sell_amount, usdc.address).return_value + + assert quote >= quote_legacy # Optimized quote must be the same or better + +def test_balancer_pricing_with_connector_equivalency(wbtc, usdc, weth, pricer, pricer_legacy): + ## 1e8 + sell_count = 10 + sell_amount = sell_count * 100000000 + + quote = pricer.getBalancerPriceWithConnectorAnalytically(wbtc.address, sell_amount, usdc.address, weth.address) + quote_legacy = pricer_legacy.getBalancerPriceWithConnector( + wbtc.address, + sell_amount, + usdc.address, + weth.address + ).return_value + + assert quote >= quote_legacy # Optimized quote must be the same or better \ No newline at end of file From b02f9445203a34a6f45005b5e541c4c91e60c812 Mon Sep 17 00:00:00 2001 From: rayeaster Date: Fri, 22 Jul 2022 10:25:16 +0800 Subject: [PATCH 21/53] add balancer test for aurabal-balethbpt pair --- tests/conftest.py | 5 +++++ tests/on_chain_pricer/test_balancer_pricer.py | 17 ++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index c0e0a31..a1ffcfb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -27,6 +27,7 @@ BALANCER_VAULT = "0xBA12222222228d8Ba445958a75a0704d566BF2C8" BVE_AURA_WETH_AURA_POOL_ID = "0xa3283e3470d3cd1f18c074e3f2d3965f6d62fff2000100000000000000000267" CVX_BVECVX_POOL = "0x04c90C198b2eFF55716079bc06d7CCc4aa4d7512" +BALETH_BPT = "0x5c6Ee304399DBdB9C8Ef030aB642B10820DB8F56" WETH_WHALE = "0xe78388b4ce79068e89bf8aa7f218ef6b9ab0e9d0" CRV = "0xD533a949740bb3306d119CC777fa900bA034cd52" @@ -66,6 +67,10 @@ def processor(lenient_contract): def oneE18(): return 1000000000000000000 +@pytest.fixture +def balethbpt(): + return interface.ERC20(BALETH_BPT) + @pytest.fixture def aurabal(): return interface.ERC20(AURABAL) diff --git a/tests/on_chain_pricer/test_balancer_pricer.py b/tests/on_chain_pricer/test_balancer_pricer.py index de7ac84..299bfb4 100644 --- a/tests/on_chain_pricer/test_balancer_pricer.py +++ b/tests/on_chain_pricer/test_balancer_pricer.py @@ -122,5 +122,20 @@ def test_get_balancer_price_aurabal_analytical(oneE18, aurabal, weth, pricer): ## there is a proper pool in Balancer for AURABAL in WETH quote = pricer.getBalancerPriceAnalytically(aurabal.address, sell_amount, weth.address) - assert quote >= p + assert quote >= p + +""" + getBalancerPrice quote for token A swapped to token B directly using given balancer pool: A - > B +""" +def test_get_balancer_price_aurabal_bpt_analytical(oneE18, aurabal, balethbpt, pricer): + ## 1e18 + sell_count = 100 + sell_amount = sell_count * oneE18 + + ## minimum quote for BAL-ETH bpt in AURABAL(1e18) https://app.balancer.fi/#/pool/0x3dd0843a028c86e0b760b1a76929d1c5ef93a2dd000200000000000000000249 + p = sell_count * 1 * oneE18 + + ## there is a proper pool in Balancer for AURABAL in BAL-ETH bpt + quote = pricer.findOptimalSwap(balethbpt.address, aurabal.address, sell_amount).return_value + assert quote[1] >= p \ No newline at end of file From 454874ef97b4d9c90b15a2a78b9a49ce244e75f7 Mon Sep 17 00:00:00 2001 From: rayeaster Date: Fri, 22 Jul 2022 11:10:55 +0800 Subject: [PATCH 22/53] house keeping for univ3 and balancer deprecated code --- contracts/OnChainPricingMainnet.sol | 116 ------------------ .../libraries/balancer/BalancerQuoter.sol | 65 ++++++++++ tests/on_chain_pricer/test_balancer_pricer.py | 40 ++---- tests/on_chain_pricer/test_univ3_pricer.py | 5 +- 4 files changed, 75 insertions(+), 151 deletions(-) create mode 100644 contracts/libraries/balancer/BalancerQuoter.sol diff --git a/contracts/OnChainPricingMainnet.sol b/contracts/OnChainPricingMainnet.sol index 691e4d1..7780980 100644 --- a/contracts/OnChainPricingMainnet.sol +++ b/contracts/OnChainPricingMainnet.sol @@ -445,74 +445,12 @@ contract OnChainPricingMainnet { } } - /// @dev query swap result from Uniswap V3 quoter for given tokenIn -> tokenOut with amountIn & fee - /// @dev this "call and revert" method consumes tons of gas - function _getUniV3QuoterQuery(address tokenIn, address tokenOut, uint24 fee, uint256 amountIn) internal returns (uint256){ - uint256 quote = IV3Quoter(UNIV3_QUOTER).quoteExactInputSingle(tokenIn, tokenOut, fee, amountIn, 0); - return quote; - } - /// @dev return token0 & token1 and if token0 equals tokenIn function _ifUniV3Token0Price(address tokenIn, address tokenOut) internal pure returns (address, address, bool){ (address token0, address token1) = tokenIn < tokenOut ? (tokenIn, tokenOut) : (tokenOut, tokenIn); return (token0, token1, token0 == tokenIn); } - /// @dev Given the address of the input token & the output token & fee tier - /// @dev with trade amount & indicator if token0 pricing required (token1/token0 e.g., token0 -> token1) - /// @dev note there are some heuristic checks around the price like pool reserve should satisfy the swap amount - /// @return the current price in V3 for it - //function _getUniV3RateHeuristic(address token0, address token1, uint24 fee, bool token0Price, uint256 amountIn) internal view returns (uint256) { - - //uint256 _t0Decimals = 10 ** IERC20Metadata(token0).decimals(); - //uint256 _t1Decimals = 10 ** IERC20Metadata(token1).decimals(); - - // Heuristically reserve with spot price check: ensure the pool tokenOut reserve makes sense in terms of thespot price [amountOutput based on slot0 price] - //(uint256 rate, uint256 amountOutput) = _getOutputWithSlot0Price(token0, token1, pool, token0Price, _t0Decimals, _t1Decimals, amountIn); - //if ((token0Price? _t1Balance : _t0Balance) <= amountOutput){ - // return 0; - //} - - // Heuristically reserves comparison check: ensure the pool [reserve comparison is consistent with the slot0 price comparison], - // i.e., asset in less amount should be more expensive in AMM pool - //bool token0MoreExpensive = _compareUniV3Tokens(token0Price, rate); - //bool token0MoreReserved = _compareUniV3TokenReserves(_t0Balance, _t1Balance, _t0Decimals, _t1Decimals); - //if (token0MoreExpensive == token0MoreReserved){ - // return 0; - //} - - //return amountOutput; - //} - - /// @dev calculate output amount according to Uniswap V3 spot price (slot0) - function _getOutputWithSlot0Price(address token0, address token1, address pool, bool token0Price, uint256 _t0Decimals, uint256 _t1Decimals, uint256 amountIn) internal view returns (uint256, uint256) { - uint256 rate = _queryUniV3PriceWithSlot(token0, token1, pool, token0Price, _t0Decimals, _t1Decimals); - uint256 amountOutput = rate * amountIn * (token0Price? _t1Decimals : _t0Decimals) / (token0Price? _t0Decimals : _t1Decimals) / 1e18; - return (rate, amountOutput); - } - - /// @dev query current price from V3 pool interface(slot0) with given pool & token0 & token1 - /// @dev and indicator if token0 pricing required (token1/token0 e.g., token0 -> token1) - /// @return the price of required token scaled with 1e18 - function _queryUniV3PriceWithSlot(address token0, address token1, address pool, bool token0Price, uint256 _t0Decimals, uint256 _t1Decimals) internal view returns (uint256) { - (uint256 sqrtPriceX96,,,,,,) = IUniswapV3Pool(pool).slot0(); - if (token0Price) { - return (((_t0Decimals * sqrtPriceX96 >> 96) * sqrtPriceX96) >> 96) * 1e18 / _t1Decimals; - } else { - return ((_t1Decimals << 192) / sqrtPriceX96 / sqrtPriceX96) * 1e18 / _t0Decimals; - } - } - - /// @dev check if token0 is more expensive than token1 given slot0 price & if token0 pricing required - //function _compareUniV3Tokens(bool token0Price, uint256 rate) internal view returns (bool) { - // return token0Price? (rate > 1e18) : (rate < 1e18); - //} - - /// @dev check if token0 reserve is bigger than token1 reserve - //function _compareUniV3TokenReserves(uint256 _t0Balance, uint256 _t1Balance, uint256 _t0Decimals, uint256 _t1Decimals) internal view returns (bool) { - // return (_t0Balance / _t0Decimals) > (_t1Balance / _t1Decimals); - //} - /// @dev query with the address of the token0 & token1 & the fee tier /// @return the uniswap v3 pool address function _getUniV3PoolAddress(address token0, address token1, uint24 fee) internal pure returns (address) { @@ -552,32 +490,6 @@ contract OnChainPricingMainnet { /// === BALANCER === /// - /// @dev Given the input/output token, returns the quote for input amount from Balancer V2 - function getBalancerPrice(address tokenIn, uint256 amountIn, address tokenOut) public returns (uint256) { - bytes32 poolId = getBalancerV2Pool(tokenIn, tokenOut); - if (poolId == BALANCERV2_NONEXIST_POOLID){ - return 0; - } - return getBalancerPriceWithinPool(poolId, tokenIn, amountIn, tokenOut); - } - - function getBalancerPriceWithinPool(bytes32 poolId, address tokenIn, uint256 amountIn, address tokenOut) public returns (uint256) { - - address[] memory assets = new address[](2); - assets[0] = tokenIn; - assets[1] = tokenOut; - - BatchSwapStep[] memory swaps = new BatchSwapStep[](1); - swaps[0] = BatchSwapStep(poolId, 0, 1, amountIn, ""); - - FundManagement memory funds = FundManagement(address(this), false, address(this), false); - - int256[] memory assetDeltas = IBalancerV2Vault(BALANCERV2_VAULT).queryBatchSwap(SwapKind.GIVEN_IN, swaps, assets, funds); - - // asset deltas: either transferring assets from the sender (for positive deltas) or to the recipient (for negative deltas). - return assetDeltas.length > 0 ? uint256(0 - assetDeltas[assetDeltas.length - 1]) : 0; - } - /// @dev Given the input/output token, returns the quote for input amount from Balancer V2 using its underlying math function getBalancerPriceAnalytically(address tokenIn, uint256 amountIn, address tokenOut) public view returns (uint256) { bytes32 poolId = getBalancerV2Pool(tokenIn, tokenOut); @@ -631,34 +543,6 @@ contract OnChainPricingMainnet { return type(uint256).max; } - /// @dev Given the input/output/connector token, returns the quote for input amount from Balancer V2 - function getBalancerPriceWithConnector(address tokenIn, uint256 amountIn, address tokenOut, address connectorToken) public returns (uint256) { - bytes32 firstPoolId = getBalancerV2Pool(tokenIn, connectorToken); - if (firstPoolId == BALANCERV2_NONEXIST_POOLID){ - return 0; - } - bytes32 secondPoolId = getBalancerV2Pool(connectorToken, tokenOut); - if (secondPoolId == BALANCERV2_NONEXIST_POOLID){ - return 0; - } - - address[] memory assets = new address[](3); - assets[0] = tokenIn; - assets[1] = connectorToken; - assets[2] = tokenOut; - - BatchSwapStep[] memory swaps = new BatchSwapStep[](2); - swaps[0] = BatchSwapStep(firstPoolId, 0, 1, amountIn, ""); - swaps[1] = BatchSwapStep(secondPoolId, 1, 2, 0, "");// amount == 0 means use all from previous step - - FundManagement memory funds = FundManagement(address(this), false, address(this), false); - - int256[] memory assetDeltas = IBalancerV2Vault(BALANCERV2_VAULT).queryBatchSwap(SwapKind.GIVEN_IN, swaps, assets, funds); - - // asset deltas: either transferring assets from the sender (for positive deltas) or to the recipient (for negative deltas). - return assetDeltas.length > 0 ? uint256(0 - assetDeltas[assetDeltas.length - 1]) : 0; - } - /// @dev Given the input/output/connector token, returns the quote for input amount from Balancer V2 using its underlying math function getBalancerPriceWithConnectorAnalytically(address tokenIn, uint256 amountIn, address tokenOut, address connectorToken) public view returns (uint256) { if (getBalancerV2Pool(tokenIn, connectorToken) == BALANCERV2_NONEXIST_POOLID || getBalancerV2Pool(connectorToken, tokenOut) == BALANCERV2_NONEXIST_POOLID){ diff --git a/contracts/libraries/balancer/BalancerQuoter.sol b/contracts/libraries/balancer/BalancerQuoter.sol new file mode 100644 index 0000000..7479a30 --- /dev/null +++ b/contracts/libraries/balancer/BalancerQuoter.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.7.6; +pragma abicoder v2; + +enum BalancerV2SwapKind { GIVEN_IN, GIVEN_OUT } + +struct BalancerV2BatchSwapStep { + bytes32 poolId; + uint256 assetInIndex; + uint256 assetOutIndex; + uint256 amount; + bytes userData; +} + +struct BalancerV2FundManagement { + address sender; + bool fromInternalBalance; + address recipient; + bool toInternalBalance; +} + +interface IBalancerV2VaultQuoter { + function queryBatchSwap(BalancerV2SwapKind kind, BalancerV2BatchSwapStep[] calldata swaps, address[] calldata assets, BalancerV2FundManagement calldata funds) external returns (int256[] memory assetDeltas); +} + +// gas consuming quoter https://dev.balancer.fi/resources/query-how-much-x-for-y +library BalancerQuoter { + address private constant _vault = 0xBA12222222228d8Ba445958a75a0704d566BF2C8; + + function getBalancerPriceWithinPool(bytes32 poolId, address tokenIn, uint256 amountIn, address tokenOut) public returns (uint256) { + + address[] memory assets = new address[](2); + assets[0] = tokenIn; + assets[1] = tokenOut; + + BalancerV2BatchSwapStep[] memory swaps = new BalancerV2BatchSwapStep[](1); + swaps[0] = BalancerV2BatchSwapStep(poolId, 0, 1, amountIn, ""); + + BalancerV2FundManagement memory funds = BalancerV2FundManagement(address(this), false, address(this), false); + + int256[] memory assetDeltas = IBalancerV2VaultQuoter(_vault).queryBatchSwap(BalancerV2SwapKind.GIVEN_IN, swaps, assets, funds); + + // asset deltas: either transferring assets from the sender (for positive deltas) or to the recipient (for negative deltas). + return assetDeltas.length > 0 ? uint256(0 - assetDeltas[assetDeltas.length - 1]) : 0; + } + + /// @dev Given the input/output/connector token, returns the quote for input amount from Balancer V2 + function getBalancerPriceWithConnector(bytes32 firstPoolId, bytes32 secondPoolId, address tokenIn, uint256 amountIn, address tokenOut, address connectorToken) public returns (uint256) { + address[] memory assets = new address[](3); + assets[0] = tokenIn; + assets[1] = connectorToken; + assets[2] = tokenOut; + + BalancerV2BatchSwapStep[] memory swaps = new BalancerV2BatchSwapStep[](2); + swaps[0] = BalancerV2BatchSwapStep(firstPoolId, 0, 1, amountIn, ""); + swaps[1] = BalancerV2BatchSwapStep(secondPoolId, 1, 2, 0, "");// amount == 0 means use all from previous step + + BalancerV2FundManagement memory funds = BalancerV2FundManagement(address(this), false, address(this), false); + + int256[] memory assetDeltas = IBalancerV2VaultQuoter(_vault).queryBatchSwap(BalancerV2SwapKind.GIVEN_IN, swaps, assets, funds); + + // asset deltas: either transferring assets from the sender (for positive deltas) or to the recipient (for negative deltas). + return assetDeltas.length > 0 ? uint256(0 - assetDeltas[assetDeltas.length - 1]) : 0; + } +} \ No newline at end of file diff --git a/tests/on_chain_pricer/test_balancer_pricer.py b/tests/on_chain_pricer/test_balancer_pricer.py index 299bfb4..4aca0b1 100644 --- a/tests/on_chain_pricer/test_balancer_pricer.py +++ b/tests/on_chain_pricer/test_balancer_pricer.py @@ -7,7 +7,7 @@ import pytest """ - getBalancerPrice quote for token A swapped to token B directly using given balancer pool: A - > B + getBalancerPriceAnalytically quote for token A swapped to token B directly using given balancer pool: A - > B """ def test_get_balancer_price_stable_analytical(oneE18, usdc, dai, pricer): ## 1e18 @@ -19,13 +19,11 @@ def test_get_balancer_price_stable_analytical(oneE18, usdc, dai, pricer): ## there is a proper pool in Balancer for DAI in USDC poolId = pricer.BALANCERV2_DAI_USDC_USDT_POOLID() - q = pricer.getBalancerPriceWithinPool(poolId, dai.address, sell_amount, usdc.address).return_value - assert q >= p quote = pricer.getBalancerQuoteWithinPoolAnalytcially(poolId, dai.address, sell_amount, usdc.address) - assert quote == q + assert quote >= p """ - getBalancerPrice quote for token A swapped to token B directly using given balancer pool: A - > B + getBalancerPriceAnalytically quote for token A swapped to token B directly using given balancer pool: A - > B """ def test_get_balancer_price(oneE18, weth, usdc, pricer): ## 1e18 @@ -33,7 +31,7 @@ def test_get_balancer_price(oneE18, weth, usdc, pricer): ## minimum quote for ETH in USDC(1e6) p = 1 * 500 * 1000000 - quote = pricer.getBalancerPrice(weth.address, sell_amount, usdc.address).return_value + quote = pricer.getBalancerPriceAnalytically(weth.address, sell_amount, usdc.address) assert quote >= p ## price sanity check with fine liquidity @@ -42,7 +40,7 @@ def test_get_balancer_price(oneE18, weth, usdc, pricer): #assert (quote / 1000000) >= (p1 / p2) * 0.98 """ - getBalancerPriceWithConnector quote for token A swapped to token B with connector token C: A -> C -> B + getBalancerPriceWithConnectorAnalytically quote for token A swapped to token B with connector token C: A -> C -> B """ def test_get_balancer_price_with_connector(oneE18, wbtc, usdc, weth, pricer): ## 1e8 @@ -51,7 +49,7 @@ def test_get_balancer_price_with_connector(oneE18, wbtc, usdc, weth, pricer): ## minimum quote for WBTC in USDC(1e6) p = sell_count * 15000 * 1000000 - quote = pricer.getBalancerPriceWithConnector(wbtc.address, sell_amount, usdc.address, weth.address).return_value + quote = pricer.getBalancerPriceWithConnectorAnalytically(wbtc.address, sell_amount, usdc.address, weth.address) assert quote >= p ## price sanity check with dime liquidity @@ -60,17 +58,6 @@ def test_get_balancer_price_with_connector(oneE18, wbtc, usdc, weth, pricer): #p2 = get_coinmarketcap_price('3408', yourCMCKey) ## usdc #assert (quote / 1000000 / sell_count) >= (p1 / p2) * 0.75 -""" - getBalancerPrice quote for token A swapped to token B directly using given balancer pool: A - > B -""" -def test_get_balancer_price_nonexistence(oneE18, cvx, weth, pricer): - ## 1e18 - sell_amount = 100 * oneE18 - - ## no proper pool in Balancer for WETH in CVX - quote = pricer.getBalancerPrice(weth.address, sell_amount, cvx.address).return_value - assert quote == 0 - """ getBalancerPriceAnalytically quote for token A swapped to token B directly using given balancer pool: A - > B analytically """ @@ -81,20 +68,7 @@ def test_get_balancer_price_analytical(oneE18, weth, usdc, pricer): ## minimum quote for ETH in USDC(1e6) p = 1 * 500 * 1000000 quote = pricer.getBalancerPriceAnalytically(weth.address, sell_amount, usdc.address) - assert quote >= p - -""" - getBalancerPriceWithConnectorAnalytically quote for token A swapped to token B directly using given balancer pool: A - > B analytically -""" -def test_get_balancer_price_with_connector_analytical(oneE18, wbtc, usdc, weth, pricer): - ## 1e8 - sell_count = 10 - sell_amount = sell_count * 100000000 - - ## minimum quote for WBTC in USDC(1e6) - p = sell_count * 15000 * 1000000 - quote = pricer.getBalancerPriceWithConnectorAnalytically(wbtc.address, sell_amount, usdc.address, weth.address) - assert quote >= p + assert quote >= p """ getBalancerPriceAnalytically quote for token A swapped to token B directly using given balancer pool: A - > B analytically diff --git a/tests/on_chain_pricer/test_univ3_pricer.py b/tests/on_chain_pricer/test_univ3_pricer.py index c86fd4f..dc9c79c 100644 --- a/tests/on_chain_pricer/test_univ3_pricer.py +++ b/tests/on_chain_pricer/test_univ3_pricer.py @@ -7,10 +7,11 @@ """ def test_get_univ3_price_in_range(oneE18, weth, usdc, usdc_whale, pricer): ## 1e18 - sell_amount = 20 * oneE18 + sell_count = 1 + sell_amount = sell_count * oneE18 ## minimum quote for ETH in USDC(1e6) ## Rip ETH price - p = 1 * 900 * 1000000 + p = sell_count * 900 * 1000000 quote = pricer.sortUniV3Pools(weth.address, sell_amount, usdc.address) assert quote[0] >= p quoteInRange = pricer.checkUniV3InRangeLiquidity(weth.address, usdc.address, sell_amount, quote[1]) From 72248a34c00bc3bfbce3149e14d0a35496728c12 Mon Sep 17 00:00:00 2001 From: rayeaster Date: Fri, 22 Jul 2022 15:26:40 +0800 Subject: [PATCH 23/53] optimize some gas cost in uniswap v2 & v3 quote --- contracts/OnChainPricingMainnet.sol | 41 +++++++++---------- tests/on_chain_pricer/test_univ3_pricer.py | 4 +- .../on_chain_pricer/test_univ3_pricer_simu.py | 4 +- 3 files changed, 23 insertions(+), 26 deletions(-) diff --git a/contracts/OnChainPricingMainnet.sol b/contracts/OnChainPricingMainnet.sol index 7780980..2c4b3fe 100644 --- a/contracts/OnChainPricingMainnet.sol +++ b/contracts/OnChainPricingMainnet.sol @@ -52,9 +52,11 @@ contract OnChainPricingMainnet { // UniV2 address public constant UNIV2_ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; // Spookyswap bytes public constant UNIV2_POOL_INITCODE = hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f'; + address public constant UNIV2_FACTORY = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; // Sushi address public constant SUSHI_ROUTER = 0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F; bytes public constant SUSHI_POOL_INITCODE = hex'e18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303'; + address public constant SUSHI_FACTORY = 0xC0AEe478e3658e2610c5F7A4A2E1777cE9e4f2Ac; // Curve / Doesn't revert on failure address public constant CURVE_ROUTER = 0x8e764bE4288B842791989DB5b8ec067279829809; // Curve quote and swaps @@ -239,15 +241,15 @@ contract OnChainPricingMainnet { function getUniPrice(address router, address tokenIn, address tokenOut, uint256 amountIn) public view returns (uint256) { // check pool existence first before quote against it - bytes memory _initCode = (router == UNIV2_ROUTER)? UNIV2_POOL_INITCODE : SUSHI_POOL_INITCODE; - (address _pool, address _token0, address _token1) = pairForUniV2(IUniswapRouterV2(router).factory(), tokenIn, tokenOut, _initCode); + bool _univ2 = (router == UNIV2_ROUTER); + (address _pool, address _token0, address _token1) = pairForUniV2((_univ2? UNIV2_FACTORY : SUSHI_FACTORY), tokenIn, tokenOut, (_univ2? UNIV2_POOL_INITCODE : SUSHI_POOL_INITCODE)); if (!_pool.isContract()){ return 0; } bool _zeroForOne = (_token0 == tokenIn); - // Use LP token Total Supply as a quick-easy substitute for liquidity - (bool _basicCheck, uint256 _t0Balance, uint256 _t1Balance) = _checkPoolLiquidityAndBalances(_pool, IERC20(_pool).totalSupply(), _token0, _token1, _zeroForOne, amountIn); + // Use dummy magic number as a quick-easy substitute for liquidity (to avoid one SLOAD) since we have pool reserve check for it + (bool _basicCheck, uint256 _t0Balance, uint256 _t1Balance) = _checkPoolLiquidityAndBalances(_pool, 1, _token0, _token1, _zeroForOne, amountIn); return _basicCheck? getUniV2AmountOutAnalytically(amountIn, (_zeroForOne? _t0Balance : _t1Balance), (_zeroForOne? _t1Balance : _t0Balance)) : 0; //address[] memory path = new address[](2); @@ -276,7 +278,7 @@ contract OnChainPricingMainnet { amountOut = numerator / denominator; } - function pairForUniV2(address factory, address tokenA, address tokenB, bytes memory _initCode) public view returns (address, address, address) { + function pairForUniV2(address factory, address tokenA, address tokenB, bytes memory _initCode) public pure returns (address, address, address) { (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); address pair = getAddressFromBytes32Lsb(keccak256(abi.encodePacked( hex'ff', @@ -300,16 +302,14 @@ contract OnChainPricingMainnet { { // Heuristic: If we already know high TVL Pools, use those uint24 _bestFee = _useSinglePoolInUniV3(tokenIn, tokenOut); - // TODO: Can rewrite to skip the loop entire when `_bestFee` + if (_bestFee > 0) { + (,uint256 _bestOutAmt) = _checkSimulationInUniV3(tokenIn, tokenOut, amountIn, _bestFee); + return (_bestOutAmt, _bestFee); + } + for (uint256 i = 0; i < feeTypes;){ uint24 _fee = univ3_fees[i]; - // skip othter pools if there is a chosen best pool to go - if (_bestFee > 0 && _fee != _bestFee){ - unchecked { ++i; } - continue; - } - { // TODO: Partial rewrite to perform initial comparison against all simulations based on "liquidity in range" // If liq is in range, then lowest fee auto-wins @@ -350,10 +350,8 @@ contract OnChainPricingMainnet { /// @dev Uniswap V3 pool in-range liquidity check /// @return true if cross-ticks full simulation required for the swap otherwise false (in-range liquidity would satisfy the swap) - function checkUniV3InRangeLiquidity(address tokenIn, address tokenOut, uint256 amountIn, uint24 _fee) public view returns (bool, uint256){ - (address token0, address token1, bool token0Price) = _ifUniV3Token0Price(tokenIn, tokenOut); + function checkUniV3InRangeLiquidity(address token0, address token1, uint256 amountIn, uint24 _fee, bool token0Price, address _pool) public view returns (bool, uint256){ { - address _pool = _getUniV3PoolAddress(token0, token1, _fee); if (!_pool.isContract()) { return (false, 0); } @@ -372,18 +370,19 @@ contract OnChainPricingMainnet { function _checkSimulationInUniV3(address tokenIn, address tokenOut, uint256 amountIn, uint24 _fee) internal view returns (bool, uint256) { bool _crossTick; uint256 _outAmt; - // TODO: Both `checkUniV3InRangeLiquidity` and `simulateUniV3Swap` recompute pool address - // Refactor to only compute once and pass + + (address token0, address token1, bool token0Price) = _ifUniV3Token0Price(tokenIn, tokenOut); + address _pool = _getUniV3PoolAddress(token0, token1, _fee); { // in-range swap check: find out whether the swap within current liquidity would move the price across next tick - (bool _outOfInRange, uint256 _outputAmount) = checkUniV3InRangeLiquidity(tokenIn, tokenOut, amountIn, _fee); + (bool _outOfInRange, uint256 _outputAmount) = checkUniV3InRangeLiquidity(token0, token1, amountIn, _fee, token0Price, _pool); _crossTick = _outOfInRange; _outAmt = _outputAmount; } { // unfortunately we need to do a full simulation to cross ticks if (_crossTick){ - _outAmt = simulateUniV3Swap(tokenIn, amountIn, tokenOut, _fee); + _outAmt = simulateUniV3Swap(token0, amountIn, token1, _fee, token0Price, _pool); } } return (_crossTick, _outAmt); @@ -416,9 +415,7 @@ contract OnChainPricingMainnet { /// @dev simulate Uniswap V3 swap using its tick-based math for given parameters /// @dev check helper UniV3SwapSimulator for more - function simulateUniV3Swap(address tokenIn, uint256 amountIn, address tokenOut, uint24 _fee) public view returns (uint256){ - (address token0, address token1, bool token0Price) = _ifUniV3Token0Price(tokenIn, tokenOut); - address _pool = _getUniV3PoolAddress(token0, token1, _fee); + function simulateUniV3Swap(address token0, uint256 amountIn, address token1, uint24 _fee, bool token0Price, address _pool) public view returns (uint256) { return IUniswapV3Simulator(uniV3Simulator).simulateUniV3Swap(_pool, token0, token1, token0Price, _fee, amountIn); } diff --git a/tests/on_chain_pricer/test_univ3_pricer.py b/tests/on_chain_pricer/test_univ3_pricer.py index dc9c79c..ca2195c 100644 --- a/tests/on_chain_pricer/test_univ3_pricer.py +++ b/tests/on_chain_pricer/test_univ3_pricer.py @@ -14,7 +14,7 @@ def test_get_univ3_price_in_range(oneE18, weth, usdc, usdc_whale, pricer): p = sell_count * 900 * 1000000 quote = pricer.sortUniV3Pools(weth.address, sell_amount, usdc.address) assert quote[0] >= p - quoteInRange = pricer.checkUniV3InRangeLiquidity(weth.address, usdc.address, sell_amount, quote[1]) + quoteInRange = pricer.checkUniV3InRangeLiquidity(usdc.address, weth.address, sell_amount, quote[1], False, "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640") assert quote[0] == quoteInRange[1] ## check against quoter @@ -36,7 +36,7 @@ def test_get_univ3_price_cross_tick(oneE18, weth, usdc, usdc_whale, pricer): p = sell_count * 900 * 1000000 quote = pricer.sortUniV3Pools(weth.address, sell_amount, usdc.address) assert quote[0] >= p - quoteCrossTicks = pricer.simulateUniV3Swap(weth.address, sell_amount, usdc.address, quote[1]) + quoteCrossTicks = pricer.simulateUniV3Swap(usdc.address, sell_amount, weth.address, quote[1], False, "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640") assert quote[0] == quoteCrossTicks ## check against quoter diff --git a/tests/on_chain_pricer/test_univ3_pricer_simu.py b/tests/on_chain_pricer/test_univ3_pricer_simu.py index fa3de8a..7073949 100644 --- a/tests/on_chain_pricer/test_univ3_pricer_simu.py +++ b/tests/on_chain_pricer/test_univ3_pricer_simu.py @@ -12,7 +12,7 @@ def test_simu_univ3_swap(oneE18, weth, usdc, pricer): ## minimum quote for ETH in USDC(1e6) ## Rip ETH price p = sell_count * 900 * 1000000 - quote = pricer.simulateUniV3Swap(weth.address, sell_amount, usdc.address, 500) + quote = pricer.simulateUniV3Swap(usdc.address, sell_amount, weth.address, 500, False, "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640") assert quote >= p @@ -26,7 +26,7 @@ def test_simu_univ3_swap2(oneE18, weth, wbtc, pricer): ## minimum quote for BTC in ETH(1e18) ## Rip ETH price p = sell_count * 14 * oneE18 - quote = pricer.simulateUniV3Swap(wbtc.address, sell_amount, weth.address, 500) + quote = pricer.simulateUniV3Swap(wbtc.address, sell_amount, weth.address, 500, True, "0x4585FE77225b41b697C938B018E2Ac67Ac5a20c0") assert quote >= p From c938ece73c84bb3d554219fb07a34df885760745 Mon Sep 17 00:00:00 2001 From: rayeaster Date: Fri, 22 Jul 2022 21:11:24 +0800 Subject: [PATCH 24/53] improve uniswap v3 preloaded mainstream pools --- contracts/OnChainPricingMainnet.sol | 115 +++++++++++++++------------- interfaces/uniswap/IV2Pool.sol | 7 ++ 2 files changed, 68 insertions(+), 54 deletions(-) create mode 100644 interfaces/uniswap/IV2Pool.sol diff --git a/contracts/OnChainPricingMainnet.sol b/contracts/OnChainPricingMainnet.sol index 2c4b3fe..71e9c94 100644 --- a/contracts/OnChainPricingMainnet.sol +++ b/contracts/OnChainPricingMainnet.sol @@ -9,6 +9,7 @@ import {Address} from "@oz/utils/Address.sol"; import "../interfaces/uniswap/IUniswapRouterV2.sol"; import "../interfaces/uniswap/IV3Pool.sol"; +import "../interfaces/uniswap/IV2Pool.sol"; import "../interfaces/uniswap/IV3Quoter.sol"; import "../interfaces/balancer/IBalancerV2Vault.sol"; import "../interfaces/balancer/IBalancerV2WeightedPool.sol"; @@ -208,7 +209,7 @@ contract OnChainPricingMainnet { quotes[4] = Quote(SwapType.BALANCER, getBalancerPriceAnalytically(tokenIn, amountIn, tokenOut), dummyPools, dummyPoolFees); if(!wethInvolved){ - quotes[5] = Quote(SwapType.UNIV3WITHWETH, getUniV3PriceWithConnector(tokenIn, amountIn, tokenOut, WETH), dummyPools, dummyPoolFees); + quotes[5] = Quote(SwapType.UNIV3WITHWETH, (_useSinglePoolInUniV3(tokenIn, tokenOut) > 0 ? 0 : getUniV3PriceWithConnector(tokenIn, amountIn, tokenOut, WETH)), dummyPools, dummyPoolFees); quotes[6] = Quote(SwapType.BALANCERWITHWETH, getBalancerPriceWithConnectorAnalytically(tokenIn, amountIn, tokenOut, WETH), dummyPools, dummyPoolFees); } @@ -248,8 +249,9 @@ contract OnChainPricingMainnet { } bool _zeroForOne = (_token0 == tokenIn); - // Use dummy magic number as a quick-easy substitute for liquidity (to avoid one SLOAD) since we have pool reserve check for it - (bool _basicCheck, uint256 _t0Balance, uint256 _t1Balance) = _checkPoolLiquidityAndBalances(_pool, 1, _token0, _token1, _zeroForOne, amountIn); + (uint256 _t0Balance, uint256 _t1Balance, ) = IUniswapV2Pool(_pool).getReserves(); + // Use dummy magic number as a quick-easy substitute for liquidity (to avoid one SLOAD) since we have pool reserve check in it + bool _basicCheck = _checkPoolLiquidityAndBalances(1, (_zeroForOne? _t0Balance : _t1Balance), amountIn); return _basicCheck? getUniV2AmountOutAnalytically(amountIn, (_zeroForOne? _t0Balance : _t1Balance), (_zeroForOne? _t1Balance : _t0Balance)) : 0; //address[] memory path = new address[](2); @@ -295,40 +297,56 @@ contract OnChainPricingMainnet { /// @dev check helper UniV3SwapSimulator for more /// @return maximum output (with current in-range liquidity & spot price) and according pool fee function sortUniV3Pools(address tokenIn, uint256 amountIn, address tokenOut) public view returns (uint256, uint24){ - uint256 feeTypes = univ3_fees.length; - uint256 _maxInRangeQuote; - uint24 _maxInRangeFee; + uint256 _maxQuote; + uint24 _maxQuoteFee; { // Heuristic: If we already know high TVL Pools, use those uint24 _bestFee = _useSinglePoolInUniV3(tokenIn, tokenOut); - if (_bestFee > 0) { - (,uint256 _bestOutAmt) = _checkSimulationInUniV3(tokenIn, tokenOut, amountIn, _bestFee); - return (_bestOutAmt, _bestFee); + (address token0, address token1, bool token0Price) = _ifUniV3Token0Price(tokenIn, tokenOut); + + { + if (_bestFee > 0) { + (,uint256 _bestOutAmt) = _checkSimulationInUniV3(token0, token1, amountIn, _bestFee, token0Price); + return (_bestOutAmt, _bestFee); + } } - for (uint256 i = 0; i < feeTypes;){ - uint24 _fee = univ3_fees[i]; + (uint256 _maxQAmt, uint24 _maxQFee) = _simLoopAllUniV3Pools(token0, token1, amountIn, token0Price); + _maxQuote = _maxQAmt; + _maxQuoteFee = _maxQFee; + } + + return (_maxQuote, _maxQuoteFee); + } + + /// @dev loop over all possible Uniswap V3 pools to find a proper quote + function _simLoopAllUniV3Pools(address token0, address token1, uint256 amountIn, bool token0Price) internal view returns (uint256, uint24) { + uint256 _maxQuote; + uint24 _maxQuoteFee; + uint256 feeTypes = univ3_fees.length; + + for (uint256 i = 0; i < feeTypes;){ + uint24 _fee = univ3_fees[i]; - { - // TODO: Partial rewrite to perform initial comparison against all simulations based on "liquidity in range" - // If liq is in range, then lowest fee auto-wins - // Else go down fee range with liq in range - // NOTE: A tick is like a ratio, so technically X ticks can offset a fee - // Meaning we prob don't need full quote in majority of cases, but can compare number of ticks - // per pool per fee and pre-rank based on that - (bool _crossTick, uint256 _outAmt) = _checkSimulationInUniV3(tokenIn, tokenOut, amountIn, _fee); - if (_outAmt > _maxInRangeQuote){ - _maxInRangeQuote = _outAmt; - _maxInRangeFee = _fee; - } - unchecked { ++i; } + { + // TODO: Partial rewrite to perform initial comparison against all simulations based on "liquidity in range" + // If liq is in range, then lowest fee auto-wins + // Else go down fee range with liq in range + // NOTE: A tick is like a ratio, so technically X ticks can offset a fee + // Meaning we prob don't need full quote in majority of cases, but can compare number of ticks + // per pool per fee and pre-rank based on that + (bool _crossTick, uint256 _outAmt) = _checkSimulationInUniV3(token0, token1, amountIn, _fee, token0Price); + if (_outAmt > _maxQuote){ + _maxQuote = _outAmt; + _maxQuoteFee = _fee; } - } + unchecked { ++i; } + } } - return (_maxInRangeQuote, _maxInRangeFee); - } + return (_maxQuote, _maxQuoteFee); + } /// @dev tell if there exists some Uniswap V3 pool for given token pair function checkUniV3PoolsExistence(address tokenIn, address tokenOut) public view returns (bool){ @@ -356,7 +374,7 @@ contract OnChainPricingMainnet { return (false, 0); } - (bool _basicCheck,,) = _checkPoolLiquidityAndBalances(_pool, IUniswapV3Pool(_pool).liquidity(), token0, token1, token0Price, amountIn); + bool _basicCheck = _checkPoolLiquidityAndBalances(IUniswapV3Pool(_pool).liquidity(), IERC20(token0Price? token0 : token1).balanceOf(_pool), amountIn); if (!_basicCheck) { return (false, 0); } @@ -367,11 +385,10 @@ contract OnChainPricingMainnet { } /// @dev internal function to avoid stack too deep for 1) check in-range liquidity in Uniswap V3 pool 2) full cross-ticks simulation in Uniswap V3 - function _checkSimulationInUniV3(address tokenIn, address tokenOut, uint256 amountIn, uint24 _fee) internal view returns (bool, uint256) { + function _checkSimulationInUniV3(address token0, address token1, uint256 amountIn, uint24 _fee, bool token0Price) internal view returns (bool, uint256) { bool _crossTick; uint256 _outAmt; - (address token0, address token1, bool token0Price) = _ifUniV3Token0Price(tokenIn, tokenOut); address _pool = _getUniV3PoolAddress(token0, token1, _fee); { // in-range swap check: find out whether the swap within current liquidity would move the price across next tick @@ -390,12 +407,12 @@ contract OnChainPricingMainnet { /// @dev internal function for a basic sanity check pool existence and balances /// @return true if basic check pass otherwise false - function _checkPoolLiquidityAndBalances(address _pool, uint256 _liq, address _token0, address _token1, bool token0Price, uint256 amountIn) internal view returns (bool, uint256, uint256) { + function _checkPoolLiquidityAndBalances(uint256 _liq, uint256 _reserveIn, uint256 amountIn) internal view returns (bool) { { - // heuristic check0: ensure the pool [exist] and properly initiated with valid in-range liquidity + // heuristic check0: ensure the pool initiated with valid liquidity in place if (_liq == 0) { - return (false, 0, 0); + return false; } } @@ -404,12 +421,11 @@ contract OnChainPricingMainnet { // Is there any change that slot0 gives us more information about the liquidity in range, // Such that optimistically it would immediately allow us to determine a winning pool? // Prob winning pool would be: Lowest Fee, with Liquidity covered within the tick - uint256 _t0Balance = IERC20(_token0).balanceOf(_pool); - uint256 _t1Balance = IERC20(_token1).balanceOf(_pool); - // heuristic check1: ensure the pool tokenIn reserve makes sense in terms of [amountIn], i.e., the pool is liquid compared to swap amount - // _t0Balance and _t1Balance will be both above zero if there is liquidity - return ((token0Price? _t0Balance : _t1Balance) > amountIn, _t0Balance, _t1Balance); + // heuristic check1: ensure the pool tokenIn reserve makes sense in terms of [amountIn], i.e., the pool is liquid compared to swap amount + // say if the pool got 100 tokenA, and you tried to swap another 100 tokenA into it for the other token, + // by the math of AMM, this will drastically imbalance the pool, so the quote won't be good for sure + return _reserveIn > amountIn; } } @@ -430,7 +446,7 @@ contract OnChainPricingMainnet { /// @return the quote for it function getUniV3PriceWithConnector(address tokenIn, uint256 amountIn, address tokenOut, address connectorToken) public view returns (uint256) { // Skip if there is a mainstrem direct swap or connector pools not exist - if (_useSinglePoolInUniV3(tokenIn, tokenOut) > 0 || !checkUniV3PoolsExistence(tokenIn, connectorToken) || !checkUniV3PoolsExistence(connectorToken, tokenOut)){ + if (!checkUniV3PoolsExistence(tokenIn, connectorToken) || !checkUniV3PoolsExistence(connectorToken, tokenOut)){ return 0; } @@ -460,26 +476,17 @@ contract OnChainPricingMainnet { /// @dev mainly 5 most-popular tokens WETH-WBTC-USDC-USDT-DAI (Volume 24H) https://info.uniswap.org/#/tokens /// @return 0 if all possible fees should be checked otherwise the ONLY pool fee we should go for function _useSinglePoolInUniV3(address tokenIn, address tokenOut) internal pure returns(uint24) { - if ((tokenIn == WETH && tokenOut == USDC) || (tokenOut == USDC && tokenIn == WETH)){ - return 500; - } else if ((tokenIn == WETH && tokenOut == WBTC) || (tokenOut == WBTC && tokenIn == WETH)){ - return 500; - } else if ((tokenIn == WETH && tokenOut == USDT) || (tokenOut == USDT && tokenIn == WETH)){ + (address token0, address token1) = tokenIn < tokenOut ? (tokenIn, tokenOut) : (tokenOut, tokenIn); + if (token1 == WETH && (token0 == USDC || token0 == WBTC || token0 == DAI)) { return 500; - } else if ((tokenIn == WETH && tokenOut == DAI) || (tokenOut == DAI && tokenIn == WETH)){ + } else if (token0 == WETH && token0 == USDT) { return 500; - } else if ((tokenIn == USDC && tokenOut == USDT) || (tokenOut == USDT && tokenIn == USDC)){ + } else if (token1 == USDC && token0 == DAI) { return 100; - } else if ((tokenIn == USDC && tokenOut == DAI) || (tokenOut == DAI && tokenIn == USDC)){ + } else if (token0 == USDC && token1 == USDT) { return 100; - } else if ((tokenIn == USDC && tokenOut == WBTC) || (tokenOut == WBTC && tokenIn == USDC)){ + } else if (token1 == USDC && token0 == WBTC) { return 3000; - } else if ((tokenIn == WBTC && tokenOut == USDT) || (tokenOut == USDT && tokenIn == WBTC)){ - return 0;// TVL too small - } else if ((tokenIn == WBTC && tokenOut == DAI) || (tokenOut == DAI && tokenIn == WBTC)){ - return 0;// TVL too small - } else if ((tokenIn == DAI && tokenOut == USDT) || (tokenOut == USDT && tokenIn == DAI)){ - return 0;// TVL too small } else { return 0; } diff --git a/interfaces/uniswap/IV2Pool.sol b/interfaces/uniswap/IV2Pool.sol new file mode 100644 index 0000000..97f5c45 --- /dev/null +++ b/interfaces/uniswap/IV2Pool.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity 0.8.10; +pragma abicoder v2; + +interface IUniswapV2Pool { + function getReserves() external view returns (uint256 reserve0, uint256 reserve1, uint32 blockTimestampLast); +} \ No newline at end of file From d63cb1ce47b6a5b01456158d8d8b77771bbdb1f1 Mon Sep 17 00:00:00 2001 From: rayeaster Date: Mon, 25 Jul 2022 16:08:07 +0800 Subject: [PATCH 25/53] add token coverage benchmark --- .../gas_benchmark/benchmark_token_coverage.py | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 tests/gas_benchmark/benchmark_token_coverage.py diff --git a/tests/gas_benchmark/benchmark_token_coverage.py b/tests/gas_benchmark/benchmark_token_coverage.py new file mode 100644 index 0000000..bd4493b --- /dev/null +++ b/tests/gas_benchmark/benchmark_token_coverage.py @@ -0,0 +1,59 @@ +import brownie +from brownie import * +import pytest + +""" + Benchmark test for token coverage in findOptimalSwap with focus in DeFi category + Selected tokens from https://defillama.com/chain/Ethereum + This file is ok to be exclcuded in test suite due to its underluying functionality should be covered by other tests + Rename the file to test_benchmark_token_coverage.py to make this part of the testing suite if required +""" + +TOP_DECIMAL18_TOKENS = [ + ("0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2", 100), # MKR + ("0x5a98fcbea516cf06857215779fd812ca3bef1b32", 10000), # LDO + ("0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", 10000), # UNI + ("0xd533a949740bb3306d119cc777fa900ba034cd52", 10000), # CRV + ("0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9", 1000), # AAVE + ("0x4e3fbd56cd56c3e72c1403e103b45db9da5b9d2b", 10000), # CVX + ("0xc00e94cb662c3520282e6f5717214004a7f26888", 1000), # COMP + ("0x6f40d4A6237C257fff2dB00FA0510DeEECd303eb", 10000), # INST + ("0xba100000625a3754423978a60c9317c58a424e3D", 10000), # BAL + ("0x3432b6a60d23ca0dfca7761b7ab56459d9c964d0", 10000), # FXS + ("0x6b3595068778dd592e39a122f4f5a5cf09c90fe2", 10000), # SUSHI + ("0x92D6C1e31e14520e676a687F0a93788B716BEff5", 10000), # DYDX + ("0x0bc529c00c6401aef6d220be8c6ea1667f6ad93e", 10), # YFI + ("0x6DEA81C8171D0bA574754EF6F8b412F2Ed88c54D", 50000), # LQTY + ("0xd33526068d116ce69f19a9ee46f0bd304f21a51f", 1000), # RPL + ("0x090185f2135308bad17527004364ebcc2d37e5f6", 10000000), # SPELL + ("0x77777feddddffc19ff86db637967013e6c6a116c", 1000), # TORN + ("0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f", 10000), # SNX + ("0x0d438f3b5175bebc262bf23753c1e53d03432bde", 1000), # WNXM + ("0xff20817765cb7f73d4bde2e66e067e58d11095c2", 10000000), # AMP + ("0xd9Fcd98c322942075A5C3860693e9f4f03AAE07b", 1000), # EUL + ("0x1f573d6fb3f13d689ff844b4ce37794d79a7ff1c", 50000), # BNT + ("0xdbdb4d16eda451d0503b854cf79d55697f90c8df", 1000), # ALCX + ("0x73968b9a57c6e53d41345fd57a6e6ae27d6cdb2f", 50000), # SDT + ("0x31429d1856ad1377a8a0079410b297e1a9e214c2", 1000000), # ANGLE + ("0x04Fa0d235C4abf4BcF4787aF4CF447DE572eF828", 10000), # UMA + ("0x6123B0049F904d730dB3C36a31167D9d4121fA6B", 50000), # RBN + ("0x956F47F50A910163D8BF957Cf5846D573E7f87CA", 10000), # FEI + ("0x853d955acef822db058eb8505911ed77f175b99e", 10000), # FRAX + ("0xD291E7a03283640FDc51b121aC401383A46cC623", 10000), # RGT + ("0x1b40183efb4dd766f11bda7a7c3ad8982e998421", 50000), # VSP + ("0x0cec1a9154ff802e7934fc916ed7ca50bde6844e", 50000), # POOL + ("0x43dfc4159d86f3a37a5a4b3d4580b888ad7d4ddd", 50000), # DODO + ("0xe28b3b32b6c345a34ff64674606124dd5aceca30", 10000), # INJ + ("0x0f2d719407fdbeff09d87557abb7232601fd9f29", 10000), # SYN +] + +@pytest.mark.parametrize("token,count", TOP_DECIMAL18_TOKENS) +def test_token_decimal18(oneE18, weth, token, count, pricer): + sell_token = token + ## 1e18 + sell_count = count + sell_amount = sell_count * oneE18 ## 1e18 + + tx = pricer.findOptimalSwap(sell_token, weth.address, sell_amount) + assert tx.return_value[1] > 0 + \ No newline at end of file From 842c5e08c7cb6aef59e7ebd09da758708d5543ea Mon Sep 17 00:00:00 2001 From: rayeaster Date: Mon, 25 Jul 2022 21:48:13 +0800 Subject: [PATCH 26/53] improve pricer code coverage --- contracts/OnChainPricingMainnet.sol | 2 +- tests/conftest.py | 15 +++++ tests/on_chain_pricer/test_balancer_pricer.py | 16 ++++++ .../test_bribe_tokens_supported.py | 13 +++++ .../on_chain_pricer/test_univ3_pricer_simu.py | 55 ++++++++++++++++++- 5 files changed, 99 insertions(+), 2 deletions(-) diff --git a/contracts/OnChainPricingMainnet.sol b/contracts/OnChainPricingMainnet.sol index 71e9c94..071ea7c 100644 --- a/contracts/OnChainPricingMainnet.sol +++ b/contracts/OnChainPricingMainnet.sol @@ -479,7 +479,7 @@ contract OnChainPricingMainnet { (address token0, address token1) = tokenIn < tokenOut ? (tokenIn, tokenOut) : (tokenOut, tokenIn); if (token1 == WETH && (token0 == USDC || token0 == WBTC || token0 == DAI)) { return 500; - } else if (token0 == WETH && token0 == USDT) { + } else if (token0 == WETH && token1 == USDT) { return 500; } else if (token1 == USDC && token0 == DAI) { return 100; diff --git a/tests/conftest.py b/tests/conftest.py index b3aeb34..867340b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -28,6 +28,9 @@ BVE_AURA_WETH_AURA_POOL_ID = "0xa3283e3470d3cd1f18c074e3f2d3965f6d62fff2000100000000000000000267" CVX_BVECVX_POOL = "0x04c90C198b2eFF55716079bc06d7CCc4aa4d7512" BALETH_BPT = "0x5c6Ee304399DBdB9C8Ef030aB642B10820DB8F56" +USDT = "0xdac17f958d2ee523a2206206994597c13d831ec7" +TUSD = "0x0000000000085d4780B73119b644AE5ecd22b376" +XSUSHI = "0x8798249c2E607446EfB7Ad49eC89dD1865Ff4272" WETH_WHALE = "0xe78388b4ce79068e89bf8aa7f218ef6b9ab0e9d0" CRV = "0xD533a949740bb3306d119CC777fa900bA034cd52" @@ -71,6 +74,18 @@ def processor(lenient_contract): def oneE18(): return 1000000000000000000 +@pytest.fixture +def xsushi(): + return interface.ERC20(XSUSHI) + +@pytest.fixture +def tusd(): + return interface.ERC20(TUSD) + +@pytest.fixture +def usdt(): + return interface.ERC20(USDT) + @pytest.fixture def balethbpt(): return interface.ERC20(BALETH_BPT) diff --git a/tests/on_chain_pricer/test_balancer_pricer.py b/tests/on_chain_pricer/test_balancer_pricer.py index 4aca0b1..382c109 100644 --- a/tests/on_chain_pricer/test_balancer_pricer.py +++ b/tests/on_chain_pricer/test_balancer_pricer.py @@ -112,4 +112,20 @@ def test_get_balancer_price_aurabal_bpt_analytical(oneE18, aurabal, balethbpt, p ## there is a proper pool in Balancer for AURABAL in BAL-ETH bpt quote = pricer.findOptimalSwap(balethbpt.address, aurabal.address, sell_amount).return_value assert quote[1] >= p + +def test_balancer_not_supported_tokens(oneE18, tusd, usdc, pricer): + ## tokenIn not in the given balancer pool + with brownie.reverts("!inBAL"): + supported = pricer.getBalancerQuoteWithinPoolAnalytcially(pricer.BALANCERV2_DAI_USDC_USDT_POOLID(), tusd.address, 1000 * oneE18, usdc.address) + ## tokenOut not in the given balancer pool + with brownie.reverts("!outBAL"): + supported = pricer.getBalancerQuoteWithinPoolAnalytcially(pricer.BALANCERV2_DAI_USDC_USDT_POOLID(), usdc.address, 1000 * 1000000, tusd.address) + +def test_get_balancer_with_connector_no_second_pair(oneE18, balethbpt, badger, weth, pricer): + ## 1e18 + sell_amount = 1000 * oneE18 + + ## no swap path for BALETHBPT -> WETH -> BADGER in Balancer V2 + quoteInRangeAndFee = pricer.getBalancerPriceWithConnectorAnalytically(balethbpt.address, sell_amount, badger.address, weth.address) + assert quoteInRangeAndFee == 0 \ No newline at end of file diff --git a/tests/on_chain_pricer/test_bribe_tokens_supported.py b/tests/on_chain_pricer/test_bribe_tokens_supported.py index 56a0591..68ee52b 100644 --- a/tests/on_chain_pricer/test_bribe_tokens_supported.py +++ b/tests/on_chain_pricer/test_bribe_tokens_supported.py @@ -71,5 +71,18 @@ def test_bribes_get_optimal_quote(pricer, token): res = pricer.findOptimalSwap(token, WETH, AMOUNT).return_value assert res[1] > 0 + +def test_only_sushi_support(oneE18, xsushi, usdc, pricer): + ## 1e18 + sell_amount = 100 * oneE18 + + supported = pricer.isPairSupported(xsushi.address, usdc.address, sell_amount) + assert supported == True + +def test_only_curve_support(oneE18, usdc, pricer): + ## 1e18 + sell_amount = 1000 * oneE18 + supported = pricer.isPairSupported("0x2a54ba2964c8cd459dc568853f79813a60761b58", usdc.address, sell_amount) + assert supported == True diff --git a/tests/on_chain_pricer/test_univ3_pricer_simu.py b/tests/on_chain_pricer/test_univ3_pricer_simu.py index 7073949..2841dca 100644 --- a/tests/on_chain_pricer/test_univ3_pricer_simu.py +++ b/tests/on_chain_pricer/test_univ3_pricer_simu.py @@ -44,4 +44,57 @@ def test_simu_univ3_swap_sort_pools(oneE18, dai, usdc, weth, pricer): ## min price assert quoteInRangeAndFee[0] >= p - assert quoteInRangeAndFee[1] == 100 ## fee-0.01% pool got better quote than fee-0.05% pool \ No newline at end of file + assert quoteInRangeAndFee[1] == 100 ## fee-0.01% pool got better quote than fee-0.05% pool + +def test_simu_univ3_swap_sort_pools_usdt(oneE18, usdt, weth, pricer): + ## 1e18 + sell_amount = 10 * oneE18 + + ## minimum quote for WETH in USDT(1e6) + p = 10 * 600 * 1000000 + quoteInRangeAndFee = pricer.sortUniV3Pools(weth.address, sell_amount, usdt.address) + + ## min price + assert quoteInRangeAndFee[0] >= p + assert quoteInRangeAndFee[1] == 500 ## fee-0.05% pool + +def test_simu_univ3_swap_usdt_usdc(oneE18, usdt, usdc, pricer): + ## 1e18 + sell_amount = 10000 * 1000000 + + ## minimum quote for USDC in USDT(1e6) + p = 10000 * 0.999 * 1000000 + quoteInRangeAndFee = pricer.sortUniV3Pools(usdc.address, sell_amount, usdt.address) + + ## min price + assert quoteInRangeAndFee[0] >= p + assert quoteInRangeAndFee[1] == 100 ## fee-0.01% pool + +def test_simu_univ3_swap_tusd_usdc(oneE18, tusd, usdc, pricer): + ## 1e18 + sell_amount = 10000 * 1000000 + + ## minimum quote for USDC in TUSD(1e18) + p = 10000 * 0.999 * oneE18 + quoteInRangeAndFee = pricer.sortUniV3Pools(usdc.address, sell_amount, tusd.address) + + ## min price + assert quoteInRangeAndFee[0] >= p + assert quoteInRangeAndFee[1] == 100 ## fee-0.01% pool + +def test_get_univ3_with_connector_no_second_pair(oneE18, balethbpt, usdc, weth, pricer): + ## 1e18 + sell_amount = 10000 * 1000000 + + ## no swap path for USDC -> WETH -> BALETHBPT in Uniswap V3 + quoteInRangeAndFee = pricer.getUniV3PriceWithConnector(usdc.address, sell_amount, balethbpt.address, weth.address) + assert quoteInRangeAndFee == 0 + +def test_get_univ3_with_connector_first_pair_quote_zero(oneE18, badger, usdc, weth, pricer): + ## 1e18 + sell_amount = 10000 * 1000000 + + ## not enough liquidity for path for BADGER -> WETH -> USDC in Uniswap V3 + quoteInRangeAndFee = pricer.getUniV3PriceWithConnector(badger.address, sell_amount, usdc.address, weth.address) + assert quoteInRangeAndFee == 0 + \ No newline at end of file From da53e8e14ea9679038f005f96c89a6bc15830a79 Mon Sep 17 00:00:00 2001 From: rayeaster Date: Tue, 26 Jul 2022 11:50:26 +0800 Subject: [PATCH 27/53] fix univ3 quote bug --- contracts/libraries/uniswap/SwapMath.sol | 2 +- .../test_bribe_tokens_supported.py | 28 ++----------------- tests/on_chain_pricer/test_univ3_pricer.py | 22 ++++++++++++++- .../on_chain_pricer/test_univ3_pricer_simu.py | 14 ++++++++++ 4 files changed, 38 insertions(+), 28 deletions(-) diff --git a/contracts/libraries/uniswap/SwapMath.sol b/contracts/libraries/uniswap/SwapMath.sol index eb69bc6..9c13ca2 100644 --- a/contracts/libraries/uniswap/SwapMath.sol +++ b/contracts/libraries/uniswap/SwapMath.sol @@ -106,7 +106,7 @@ library SwapMath { uint256 amountRemainingLessFee = FullMath.mulDiv(_exactInParams._amountIn, 1e6 - (_exactInParams._fee), 1e6); uint256 amountIn = _exactInParams._zeroForOne? SqrtPriceMath.getAmount0Delta(_exactInParams._targetPriceX96, _exactInParams._currentPriceX96, _exactInParams._liquidity, true) : SqrtPriceMath.getAmount1Delta(_exactInParams._currentPriceX96, _exactInParams._targetPriceX96, _exactInParams._liquidity, true); - if (amountRemainingLessFee >= _exactInParams._amountIn) sqrtRatioNextX96 = _exactInParams._targetPriceX96; + if (amountRemainingLessFee >= amountIn) sqrtRatioNextX96 = _exactInParams._targetPriceX96; else sqrtRatioNextX96 = SqrtPriceMath.getNextSqrtPriceFromInput(_exactInParams._currentPriceX96, _exactInParams._liquidity, amountRemainingLessFee, _exactInParams._zeroForOne); return (amountIn, sqrtRatioNextX96); } diff --git a/tests/on_chain_pricer/test_bribe_tokens_supported.py b/tests/on_chain_pricer/test_bribe_tokens_supported.py index 68ee52b..d48b03a 100644 --- a/tests/on_chain_pricer/test_bribe_tokens_supported.py +++ b/tests/on_chain_pricer/test_bribe_tokens_supported.py @@ -58,31 +58,7 @@ def test_are_bribes_supported(pricer, token): res = pricer.isPairSupported(token, WETH, AMOUNT) assert res - -@pytest.mark.parametrize("token", TOKENS_18_DECIMALS) -def test_bribes_get_optimal_quote(pricer, token): - """ - Given a bunch of tokens historically used as bribes, verifies the pricer will return non-zero value - We sell all to WETH which is pretty realistic - """ - - ## 1e18 for everything, even with insane slippage will still return non-zero which is sufficient at this time - AMOUNT = 1e18 - - res = pricer.findOptimalSwap(token, WETH, AMOUNT).return_value - assert res[1] > 0 -def test_only_sushi_support(oneE18, xsushi, usdc, pricer): - ## 1e18 - sell_amount = 100 * oneE18 - - supported = pricer.isPairSupported(xsushi.address, usdc.address, sell_amount) - assert supported == True - -def test_only_curve_support(oneE18, usdc, pricer): - ## 1e18 - sell_amount = 1000 * oneE18 - - supported = pricer.isPairSupported("0x2a54ba2964c8cd459dc568853f79813a60761b58", usdc.address, sell_amount) - assert supported == True + quote = pricer.findOptimalSwap.call(token, WETH, AMOUNT) + assert quote[1] > 0 diff --git a/tests/on_chain_pricer/test_univ3_pricer.py b/tests/on_chain_pricer/test_univ3_pricer.py index ca2195c..eac007e 100644 --- a/tests/on_chain_pricer/test_univ3_pricer.py +++ b/tests/on_chain_pricer/test_univ3_pricer.py @@ -72,4 +72,24 @@ def test_get_univ3_price_with_connector_stablecoin(oneE18, dai, usdc, weth, pric quoteWithConnector = pricer.getUniV3PriceWithConnector(dai.address, sell_amount, usdc.address, weth.address) ## min price - assert quoteWithConnector >= p \ No newline at end of file + assert quoteWithConnector >= p + +""" + test case for COW token to fix reported issue https://github.com/GalloDaSballo/fair-selling/issues/26 +""" +def test_get_univ3_price_cow(oneE18, weth, usdc_whale, pricer): + ## 1e18 + token = "0xdef1ca1fb7fbcdc777520aa7f396b4e015f497ab" + sell_count = 12209 + sell_amount = sell_count * oneE18 + + ## minimum quote for COW in ETH(1e18) + p = sell_count * 0.00005 * oneE18 + quote = pricer.sortUniV3Pools(token, sell_amount, weth.address) + print(quote) + assert quote[0] >= p + assert quote[1] == 10000 + + ## check against quoter + quoterP = interface.IV3Quoter(pricer.UNIV3_QUOTER()).quoteExactInputSingle.call(token, weth.address, quote[1], sell_amount, 0, {'from': usdc_whale.address}) + assert quoterP == quote[0] \ No newline at end of file diff --git a/tests/on_chain_pricer/test_univ3_pricer_simu.py b/tests/on_chain_pricer/test_univ3_pricer_simu.py index 2841dca..f54c475 100644 --- a/tests/on_chain_pricer/test_univ3_pricer_simu.py +++ b/tests/on_chain_pricer/test_univ3_pricer_simu.py @@ -97,4 +97,18 @@ def test_get_univ3_with_connector_first_pair_quote_zero(oneE18, badger, usdc, we ## not enough liquidity for path for BADGER -> WETH -> USDC in Uniswap V3 quoteInRangeAndFee = pricer.getUniV3PriceWithConnector(badger.address, sell_amount, usdc.address, weth.address) assert quoteInRangeAndFee == 0 + +def test_only_sushi_support(oneE18, xsushi, usdc, pricer): + ## 1e18 + sell_amount = 100 * oneE18 + + supported = pricer.isPairSupported(xsushi.address, usdc.address, sell_amount) + assert supported == True + +def test_only_curve_support(oneE18, usdc, pricer): + ## 1e18 + sell_amount = 1000 * oneE18 + + supported = pricer.isPairSupported("0x2a54ba2964c8cd459dc568853f79813a60761b58", usdc.address, sell_amount) + assert supported == True \ No newline at end of file From b7f24546d27967e9374f1bdcc8720b20c7f7ccfa Mon Sep 17 00:00:00 2001 From: rayeaster Date: Tue, 26 Jul 2022 16:13:50 +0800 Subject: [PATCH 28/53] add more pool coverage for balancer v2 --- contracts/OnChainPricingMainnet.sol | 46 +++++++++---------- tests/gas_benchmark/benchmark_pricer_gas.py | 4 +- .../gas_benchmark/benchmark_token_coverage.py | 4 +- tests/on_chain_pricer/test_balancer_pricer.py | 19 +++++++- 4 files changed, 45 insertions(+), 28 deletions(-) diff --git a/contracts/OnChainPricingMainnet.sol b/contracts/OnChainPricingMainnet.sol index 071ea7c..0eb333c 100644 --- a/contracts/OnChainPricingMainnet.sol +++ b/contracts/OnChainPricingMainnet.sol @@ -107,7 +107,7 @@ contract OnChainPricingMainnet { bytes32 public constant BALANCERV2_AURABAL_BALWETH_POOLID = 0x3dd0843a028c86e0b760b1a76929d1c5ef93a2dd000200000000000000000249; address public constant GRAVIAURA = 0xBA485b556399123261a5F9c95d413B4f93107407; - bytes32 public constant BALANCERV2_AURABAL_GRAVIAURA_BALWETH_POOLID = 0x0578292cb20a443ba1cde459c985ce14ca2bdee5000100000000000000000269; + bytes32 public constant BALANCERV2_AURABAL_GRAVIAURA_WETH_POOLID = 0x0578292cb20a443ba1cde459c985ce14ca2bdee5000100000000000000000269; bytes32 public constant BALANCERV2_DAI_USDC_USDT_POOLID = 0x06df3b2bbb68adc8b0e302443692037ed9f91b42000000000000000000000063;// Not used due to possible migration: https://forum.balancer.fi/t/vulnerability-disclosure/3179 address public constant AURABAL = 0x616e8BfA43F920657B3497DBf40D6b1A02D4608d; @@ -562,46 +562,46 @@ contract OnChainPricingMainnet { /// @return selected BalancerV2 pool given the tokenIn and tokenOut function getBalancerV2Pool(address tokenIn, address tokenOut) public view returns(bytes32){ - // TODO: Sort tokens so we can refactor to one check instead of two - if ((tokenIn == WETH && tokenOut == CREAM) || (tokenOut == WETH && tokenIn == CREAM)){ + (address token0, address token1) = tokenIn < tokenOut ? (tokenIn, tokenOut) : (tokenOut, tokenIn); + if (token0 == CREAM && token1 == WETH){ return BALANCERV2_CREAM_WETH_POOLID; - } else if ((tokenIn == WETH && tokenOut == GNO) || (tokenOut == WETH && tokenIn == GNO)){ + } else if (token0 == GNO && token1 == WETH){ return BALANCERV2_GNO_WETH_POOLID; - } else if ((tokenIn == WBTC && tokenOut == BADGER) || (tokenOut == WBTC && tokenIn == BADGER)){ + } else if (token0 == WBTC && token1 == BADGER){ return BALANCERV2_BADGER_WBTC_POOLID; - } else if ((tokenIn == WETH && tokenOut == FEI) || (tokenOut == WETH && tokenIn == FEI)){ + } else if (token0 == FEI && token1 == WETH){ return BALANCERV2_FEI_WETH_POOLID; - } else if ((tokenIn == WETH && tokenOut == BAL) || (tokenOut == WETH && tokenIn == BAL)){ + } else if (token0 == BAL && token1 == WETH){ return BALANCERV2_BAL_WETH_POOLID; - } else if ((tokenIn == WETH && tokenOut == USDC) || (tokenOut == WETH && tokenIn == USDC)){ + } else if (token0 == USDC && token1 == WETH){ return BALANCERV2_USDC_WETH_POOLID; - } else if ((tokenIn == WETH && tokenOut == WBTC) || (tokenOut == WETH && tokenIn == WBTC)){ + } else if (token0 == WBTC && token1 == WETH){ return BALANCERV2_WBTC_WETH_POOLID; - } else if ((tokenIn == WETH && tokenOut == WSTETH) || (tokenOut == WETH && tokenIn == WSTETH)){ + } else if (token0 == WSTETH && token1 == WETH){ return BALANCERV2_WSTETH_WETH_POOLID; - } else if ((tokenIn == WETH && tokenOut == LDO) || (tokenOut == WETH && tokenIn == LDO)){ + } else if (token0 == LDO && token1 == WETH){ return BALANCERV2_LDO_WETH_POOLID; - } else if ((tokenIn == WETH && tokenOut == SRM) || (tokenOut == WETH && tokenIn == SRM)){ + } else if (token0 == SRM && token1 == WETH){ return BALANCERV2_SRM_WETH_POOLID; - } else if ((tokenIn == WETH && tokenOut == rETH) || (tokenOut == WETH && tokenIn == rETH)){ + } else if (token0 == rETH && token1 == WETH){ return BALANCERV2_rETH_WETH_POOLID; - } else if ((tokenIn == WETH && tokenOut == AKITA) || (tokenOut == WETH && tokenIn == AKITA)){ + } else if (token0 == AKITA && token1 == WETH){ return BALANCERV2_AKITA_WETH_POOLID; - } else if ((tokenIn == WETH && tokenOut == OHM) || (tokenOut == WETH && tokenIn == OHM) || (tokenIn == DAI && tokenOut == OHM) || (tokenOut == DAI && tokenIn == OHM)){ + } else if ((token0 == OHM && token1 == WETH) || (token0 == OHM && token1 == DAI)){ return BALANCERV2_OHM_DAI_WETH_POOLID; - } else if ((tokenIn == COW && tokenOut == GNO) || (tokenOut == COW && tokenIn == GNO)){ + } else if (token0 == GNO && token1 == COW){ return BALANCERV2_COW_GNO_POOLID; - } else if ((tokenIn == WETH && tokenOut == COW) || (tokenOut == WETH && tokenIn == COW)){ + } else if (token0 == WETH && token1 == COW){ return BALANCERV2_COW_WETH_POOLID; - } else if ((tokenIn == WETH && tokenOut == AURA) || (tokenOut == WETH && tokenIn == AURA)){ + } else if (token0 == WETH && token1 == AURA){ return BALANCERV2_AURA_WETH_POOLID; - } else if ((tokenIn == BALWETHBPT && tokenOut == AURABAL) || (tokenOut == BALWETHBPT && tokenIn == AURABAL)){ + } else if (token0 == BALWETHBPT && token1 == AURABAL){ return BALANCERV2_AURABAL_BALWETH_POOLID; // TODO CHANGE - } else if ((tokenIn == WETH && tokenOut == AURABAL) || (tokenOut == WETH && tokenIn == AURABAL)){ - return BALANCERV2_AURABAL_GRAVIAURA_BALWETH_POOLID; - } else if ((tokenIn == WETH && tokenOut == GRAVIAURA) || (tokenOut == WETH && tokenIn == GRAVIAURA)){ - return BALANCERV2_AURABAL_GRAVIAURA_BALWETH_POOLID; + } else if (token0 == AURABAL && token1 == WETH){ + return BALANCERV2_AURABAL_GRAVIAURA_WETH_POOLID; + } else if (token0 == GRAVIAURA && token1 == WETH){ + return BALANCERV2_AURABAL_GRAVIAURA_WETH_POOLID; } else{ return BALANCERV2_NONEXIST_POOLID; } diff --git a/tests/gas_benchmark/benchmark_pricer_gas.py b/tests/gas_benchmark/benchmark_pricer_gas.py index d50ae0e..b4aad55 100644 --- a/tests/gas_benchmark/benchmark_pricer_gas.py +++ b/tests/gas_benchmark/benchmark_pricer_gas.py @@ -61,7 +61,7 @@ def test_gas_only_uniswap_v3(oneE18, weth, pricer): tx = pricer.findOptimalSwap(token, weth.address, sell_amount) assert tx.return_value[0] == 3 ## UNIV3 assert tx.return_value[1] > 0 - assert tx.gas_used <= 130000 ## 128409 in test simulation + assert tx.gas_used <= 150000 ## 146254 in test simulation def test_gas_only_uniswap_v3_with_weth(oneE18, wbtc, pricer): token = "0xf4d2888d29D722226FafA5d9B24F9164c092421E" # some swap (LOOKS-WETH-WBTC) only in Uniswap V3 via WETH in between as connector @@ -72,7 +72,7 @@ def test_gas_only_uniswap_v3_with_weth(oneE18, wbtc, pricer): tx = pricer.findOptimalSwap(token, wbtc.address, sell_amount) assert tx.return_value[0] == 4 ## UNIV3WITHWETH assert tx.return_value[1] > 0 - assert tx.gas_used <= 210000 ## 203586 in test simulation + assert tx.gas_used <= 230000 ## 227498 in test simulation def test_gas_almost_everything(oneE18, wbtc, weth, pricer): token = weth # some swap (WETH-WBTC) almost in every DEX, the most gas-consuming scenario diff --git a/tests/gas_benchmark/benchmark_token_coverage.py b/tests/gas_benchmark/benchmark_token_coverage.py index bd4493b..c527522 100644 --- a/tests/gas_benchmark/benchmark_token_coverage.py +++ b/tests/gas_benchmark/benchmark_token_coverage.py @@ -54,6 +54,6 @@ def test_token_decimal18(oneE18, weth, token, count, pricer): sell_count = count sell_amount = sell_count * oneE18 ## 1e18 - tx = pricer.findOptimalSwap(sell_token, weth.address, sell_amount) - assert tx.return_value[1] > 0 + quote = pricer.findOptimalSwap.call(sell_token, weth.address, sell_amount) + assert quote[1] > 0 \ No newline at end of file diff --git a/tests/on_chain_pricer/test_balancer_pricer.py b/tests/on_chain_pricer/test_balancer_pricer.py index 382c109..115d64e 100644 --- a/tests/on_chain_pricer/test_balancer_pricer.py +++ b/tests/on_chain_pricer/test_balancer_pricer.py @@ -31,6 +31,7 @@ def test_get_balancer_price(oneE18, weth, usdc, pricer): ## minimum quote for ETH in USDC(1e6) p = 1 * 500 * 1000000 + quote = pricer.getBalancerPriceAnalytically(weth.address, sell_amount, usdc.address) assert quote >= p @@ -120,7 +121,7 @@ def test_balancer_not_supported_tokens(oneE18, tusd, usdc, pricer): ## tokenOut not in the given balancer pool with brownie.reverts("!outBAL"): supported = pricer.getBalancerQuoteWithinPoolAnalytcially(pricer.BALANCERV2_DAI_USDC_USDT_POOLID(), usdc.address, 1000 * 1000000, tusd.address) - + def test_get_balancer_with_connector_no_second_pair(oneE18, balethbpt, badger, weth, pricer): ## 1e18 sell_amount = 1000 * oneE18 @@ -128,4 +129,20 @@ def test_get_balancer_with_connector_no_second_pair(oneE18, balethbpt, badger, w ## no swap path for BALETHBPT -> WETH -> BADGER in Balancer V2 quoteInRangeAndFee = pricer.getBalancerPriceWithConnectorAnalytically(balethbpt.address, sell_amount, badger.address, weth.address) assert quoteInRangeAndFee == 0 + +def test_get_balancer_pools(weth, pricer): + assert pricer.getBalancerV2Pool(pricer.GRAVIAURA(), weth.address) != pricer.BALANCERV2_NONEXIST_POOLID() ## bveaura + assert pricer.getBalancerV2Pool(pricer.AURA(), weth.address) != pricer.BALANCERV2_NONEXIST_POOLID() + assert pricer.getBalancerV2Pool(pricer.COW(), weth.address) != pricer.BALANCERV2_NONEXIST_POOLID() + assert pricer.getBalancerV2Pool(pricer.COW(), pricer.GNO()) != pricer.BALANCERV2_NONEXIST_POOLID() + assert pricer.getBalancerV2Pool(pricer.OHM(), weth.address) != pricer.BALANCERV2_NONEXIST_POOLID() + assert pricer.getBalancerV2Pool(pricer.AKITA(), weth.address) != pricer.BALANCERV2_NONEXIST_POOLID() + assert pricer.getBalancerV2Pool(pricer.AKITA(), weth.address) != pricer.BALANCERV2_NONEXIST_POOLID() + assert pricer.getBalancerV2Pool(pricer.rETH(), weth.address) != pricer.BALANCERV2_NONEXIST_POOLID() + assert pricer.getBalancerV2Pool(pricer.SRM(), weth.address) != pricer.BALANCERV2_NONEXIST_POOLID() + assert pricer.getBalancerV2Pool(pricer.WSTETH(), weth.address) != pricer.BALANCERV2_NONEXIST_POOLID() + assert pricer.getBalancerV2Pool(pricer.BAL(), weth.address) != pricer.BALANCERV2_NONEXIST_POOLID() + assert pricer.getBalancerV2Pool(pricer.GNO(), weth.address) != pricer.BALANCERV2_NONEXIST_POOLID() + assert pricer.getBalancerV2Pool(pricer.FEI(), weth.address) != pricer.BALANCERV2_NONEXIST_POOLID() + assert pricer.getBalancerV2Pool(pricer.CREAM(), weth.address) != pricer.BALANCERV2_NONEXIST_POOLID() \ No newline at end of file From 40fe8e607630b9149d23875af3d4242a979479cd Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Wed, 27 Jul 2022 03:59:15 +0200 Subject: [PATCH 29/53] feat: readme and made equivalency base test --- README.md | 27 +++ .../test_heuristic_equivalency.py | 176 ++++++++++++++++++ 2 files changed, 203 insertions(+) create mode 100644 tests/heuristic_equivalency/test_heuristic_equivalency.py diff --git a/README.md b/README.md index 02cb75e..7df27b7 100644 --- a/README.md +++ b/README.md @@ -67,3 +67,30 @@ quote = pricer.findOptimalSwap(t_in, t_out, amt_in) Variation of Pricer with a slippage tollerance + + +# Notable Tests + +## Benchmark specific AMM quotes +TODO: Improve to just use the specific quote + +``` +brownie test tests/gas_benchmark/benchmark_pricer_gas.py --gas +``` + +## Benchmark coverage of top DeFi Tokens + +TODO: Add like 200 tokens +TODO: Compare against Coingecko API or smth + +``` +brownie test tests/gas_benchmark/benchmark_token_coverage.py --gas +``` + +## Notable Test from V2 + +Run V3 Pricer against V2, to confirm results are correct, but with gas savings + +``` +brownie test tests/heuristic_equivalency/test_heuristic_equivalency.py +``` \ No newline at end of file diff --git a/tests/heuristic_equivalency/test_heuristic_equivalency.py b/tests/heuristic_equivalency/test_heuristic_equivalency.py new file mode 100644 index 0000000..e020253 --- /dev/null +++ b/tests/heuristic_equivalency/test_heuristic_equivalency.py @@ -0,0 +1,176 @@ +from brownie import chain +from rich.console import Console + +console = Console() + +""" + Evaluates the pricing quotes generated by the optimized (heuristic) version of the OnChainPricingMainnet + in contrast to its legacy version. The new version should lead to the same optimal price while consuming + less gas. + + Tests excluded from main test suite as core functionalities are not tested here. In order to add to test + suite, modify the file name to: `test_heuristic_equivalency.py`. Note that tested routes depend on current + liquidity state and, if liquidity moves between protocols, some assertions may fail. +""" + +### Test findOptimalSwap Equivalencies for different cases + +def test_pricing_equivalency_uniswap_v2(weth, pricer, pricer_legacy): + token = "0xBC7250C8c3eCA1DfC1728620aF835FCa489bFdf3" # some swap (GM-WETH) only in Uniswap V2 + ## 1e18 + sell_count = 100000000 + sell_amount = sell_count * 1000000000 ## 1e9 + + chain.snapshot() # To price under same chain conditions (just because) + tx = pricer.findOptimalSwap(token, weth.address, sell_amount) + assert tx.return_value[0] == 1 ## UNIV2 + quote = tx.return_value[1] + + chain.revert() + tx2 = pricer_legacy.findOptimalSwap(token, weth.address, sell_amount) + assert tx2.return_value[0] == 1 ## UNIV2 + quote_legacy = tx2.return_value[1] + + assert quote >= quote_legacy # Optimized quote must be the same or better + assert tx.gas_used < tx2.gas_used + +def test_pricing_equivalency_uniswap_v2_sushi(oneE18, weth, pricer, pricer_legacy): + token = "0x2e9d63788249371f1DFC918a52f8d799F4a38C94" # some swap (TOKE-WETH) only in Uniswap V2 & SushiSwap + ## 1e18 + sell_count = 5000 + sell_amount = sell_count * oneE18 ## 1e18 + + chain.snapshot() # To price under same chain conditions (just because) + tx = pricer.findOptimalSwap(token, weth.address, sell_amount) + assert (tx.return_value[0] == 1 or tx.return_value[0] == 2) ## UNIV2 or SUSHI + quote = tx.return_value[1] + + chain.revert() + tx2 = pricer_legacy.findOptimalSwap(token, weth.address, sell_amount) + assert (tx2.return_value[0] == 1 or tx2.return_value[0] == 2) ## UNIV2 or SUSHI + quote_legacy = tx2.return_value[1] + + assert quote >= quote_legacy # Optimized quote must be the same or better + assert tx.gas_used < tx2.gas_used + +def test_pricing_equivalency_balancer_v2(oneE18, weth, aura, pricer, pricer_legacy): + token = aura # some swap (AURA-WETH) only in Balancer V2 + ## 1e18 + sell_count = 2000 + sell_amount = sell_count * oneE18 ## 1e18 + + chain.snapshot() # To price under same chain conditions (just because) + tx = pricer.findOptimalSwap(token, weth.address, sell_amount) + assert tx.return_value[0] == 5 ## BALANCER + quote = tx.return_value[1] + + chain.revert() + tx2 = pricer_legacy.findOptimalSwap(token, weth.address, sell_amount) + assert tx2.return_value[0] == 5 ## BALANCER + quote_legacy = tx2.return_value[1] + + assert quote >= quote_legacy # Optimized quote must be the same or better + assert tx.gas_used < tx2.gas_used + +def test_pricing_equivalency_balancer_v2_with_weth(oneE18, wbtc, aura, pricer, pricer_legacy): + token = aura # some swap (AURA-WETH-WBTC) only in Balancer V2 via WETH in between as connector + ## 1e18 + sell_count = 2000 + sell_amount = sell_count * oneE18 ## 1e18 + + chain.snapshot() # To price under same chain conditions (just because) + tx = pricer.findOptimalSwap(token, wbtc.address, sell_amount) + assert tx.return_value[0] == 6 ## BALANCERWITHWETH + quote = tx.return_value[1] + + chain.revert() + tx2 = pricer_legacy.findOptimalSwap(token, wbtc.address, sell_amount) + assert tx2.return_value[0] == 6 ## BALANCERWITHWETH + quote_legacy = tx2.return_value[1] + + assert quote >= quote_legacy # Optimized quote must be the same or better + assert tx.gas_used < tx2.gas_used + +def test_pricing_equivalency_uniswap_v3(oneE18, weth, pricer, pricer_legacy): + token = "0xf4d2888d29D722226FafA5d9B24F9164c092421E" # some swap (LOOKS-WETH) only in Uniswap V3 + ## 1e18 + sell_count = 600000 + sell_amount = sell_count * oneE18 ## 1e18 + + chain.snapshot() # To price under same chain conditions (just because) + tx = pricer.findOptimalSwap(token, weth.address, sell_amount) + assert tx.return_value[0] == 3 ## UNIV3 + quote = tx.return_value[1] + + chain.revert() + tx2 = pricer_legacy.findOptimalSwap(token, weth.address, sell_amount) + assert tx2.return_value[0] == 3 ## UNIV3 + quote_legacy = tx2.return_value[1] + + assert quote >= quote_legacy # Optimized quote must be the same or better + assert tx.gas_used < tx2.gas_used + +def test_pricing_equivalency_uniswap_v3_with_weth(oneE18, wbtc, pricer, pricer_legacy): + token = "0xf4d2888d29D722226FafA5d9B24F9164c092421E" # some swap (LOOKS-WETH-WBTC) only in Uniswap V3 via WETH in between as connector + ## 1e18 + sell_count = 600000 + sell_amount = sell_count * oneE18 ## 1e18 + + chain.snapshot() # To price under same chain conditions (just because) + tx = pricer.findOptimalSwap(token, wbtc.address, sell_amount) + assert tx.return_value[0] == 4 ## UNIV3WITHWETH + quote = tx.return_value[1] + + chain.revert() + tx2 = pricer_legacy.findOptimalSwap(token, wbtc.address, sell_amount) + assert tx2.return_value[0] == 4 ## UNIV3WITHWETH + quote_legacy = tx2.return_value[1] + + assert quote >= quote_legacy # Optimized quote must be the same or better + assert tx.gas_used < tx2.gas_used + +def test_pricing_equivalency_almost_everything(oneE18, wbtc, weth, pricer, pricer_legacy): + token = weth # some swap (WETH-WBTC) almost in every DEX, the most gas-consuming scenario + ## 1e18 + sell_count = 10 + sell_amount = sell_count * oneE18 ## 1e18 + + chain.snapshot() # To price under same chain conditions (just because) + tx = pricer.findOptimalSwap(token, wbtc.address, sell_amount) + assert (tx.return_value[0] <= 3 or tx.return_value[0] == 5) ## CURVE or UNIV2 or SUSHI or UNIV3 or BALANCER + quote = tx.return_value[1] + + chain.revert() + tx2 = pricer_legacy.findOptimalSwap(token, wbtc.address, sell_amount) + assert (tx2.return_value[0] <= 3 or tx2.return_value[0] == 5) ## CURVE or UNIV2 or SUSHI or UNIV3 or BALANCER + quote_legacy = tx2.return_value[1] + + assert quote >= quote_legacy # Optimized quote must be the same or better + assert tx.gas_used < tx2.gas_used + + +### Test specific pricing functions for different underlying protocols + +def test_balancer_pricing_equivalency(oneE18, weth, usdc, pricer, pricer_legacy): + ## 1e18 + sell_amount = 1 * oneE18 + + quote = pricer.getBalancerPriceAnalytically(weth.address, sell_amount, usdc.address) + quote_legacy = pricer_legacy.getBalancerPrice(weth.address, sell_amount, usdc.address).return_value + + assert quote >= quote_legacy # Optimized quote must be the same or better + +def test_balancer_pricing_with_connector_equivalency(wbtc, usdc, weth, pricer, pricer_legacy): + ## 1e8 + sell_count = 10 + sell_amount = sell_count * 100000000 + + quote = pricer.getBalancerPriceWithConnectorAnalytically(wbtc.address, sell_amount, usdc.address, weth.address) + quote_legacy = pricer_legacy.getBalancerPriceWithConnector( + wbtc.address, + sell_amount, + usdc.address, + weth.address + ).return_value + + assert quote >= quote_legacy # Optimized quote must be the same or better \ No newline at end of file From 6ee21302fa2db9a1521fdfde24c698cc02d91dd8 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Wed, 27 Jul 2022 03:59:49 +0200 Subject: [PATCH 30/53] gas: `univ3_fees` is a function --- contracts/OnChainPricingMainnet.sol | 46 +++++++++++++++++++---------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/contracts/OnChainPricingMainnet.sol b/contracts/OnChainPricingMainnet.sol index 0eb333c..833dd6e 100644 --- a/contracts/OnChainPricingMainnet.sol +++ b/contracts/OnChainPricingMainnet.sol @@ -66,8 +66,7 @@ contract OnChainPricingMainnet { address public constant UNIV3_QUOTER = 0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6; bytes32 public constant UNIV3_POOL_INIT_CODE_HASH = 0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54; address public constant UNIV3_FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984; - uint24[4] univ3_fees = [uint24(100), 500, 3000, 10000]; - + // BalancerV2 Vault address public constant BALANCERV2_VAULT = 0xBA12222222228d8Ba445958a75a0704d566BF2C8; bytes32 public constant BALANCERV2_NONEXIST_POOLID = "BALANCER-V2-NON-EXIST-POOLID"; @@ -114,11 +113,28 @@ contract OnChainPricingMainnet { address public constant BALWETHBPT = 0x5c6Ee304399DBdB9C8Ef030aB642B10820DB8F56; uint256 public constant CURVE_FEE_SCALE = 100000; address public constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; - + + // TODO: Consider making immutable /// @dev helper library to simulate Uniswap V3 swap address public uniV3Simulator; /// @dev helper library to simulate Balancer V2 swap address public balancerV2Simulator; + + + /// UniV3, replaces an array + uint256 constant univ3_fees_length = 4; + function univ3_fees(uint256 i) internal pure returns (uint24) { + if(i == 0){ + return uint24(100); + } else if (i == 1) { + return uint24(500); + } else if (i == 2) { + return uint24(3000); + } else if (i == 3) { + return uint24(10000); + } + } + /// === TEST-ONLY === constructor(address _uniV3Simulator, address _balancerV2Simulator){ @@ -126,15 +142,15 @@ contract OnChainPricingMainnet { balancerV2Simulator = _balancerV2Simulator; } - function setUniV3Simulator(address _uniV3Simulator) external { - require(_uniV3Simulator != address(0));//TODO permission - uniV3Simulator = _uniV3Simulator; - } + // function setUniV3Simulator(address _uniV3Simulator) external { + // require(_uniV3Simulator != address(0));//TODO permission + // uniV3Simulator = _uniV3Simulator; + // } - function setBalancerV2Simulator(address _balancerV2Simulator) external { - require(_balancerV2Simulator != address(0));//TODO permission - balancerV2Simulator = _balancerV2Simulator; - } + // function setBalancerV2Simulator(address _balancerV2Simulator) external { + // require(_balancerV2Simulator != address(0));//TODO permission + // balancerV2Simulator = _balancerV2Simulator; + // } /// === END TEST-ONLY === struct Quote { @@ -324,10 +340,10 @@ contract OnChainPricingMainnet { function _simLoopAllUniV3Pools(address token0, address token1, uint256 amountIn, bool token0Price) internal view returns (uint256, uint24) { uint256 _maxQuote; uint24 _maxQuoteFee; - uint256 feeTypes = univ3_fees.length; + uint256 feeTypes = univ3_fees_length; for (uint256 i = 0; i < feeTypes;){ - uint24 _fee = univ3_fees[i]; + uint24 _fee = univ3_fees(i); { // TODO: Partial rewrite to perform initial comparison against all simulations based on "liquidity in range" @@ -350,12 +366,12 @@ contract OnChainPricingMainnet { /// @dev tell if there exists some Uniswap V3 pool for given token pair function checkUniV3PoolsExistence(address tokenIn, address tokenOut) public view returns (bool){ - uint256 feeTypes = univ3_fees.length; + uint256 feeTypes = univ3_fees_length; (address token0, address token1, bool token0Price) = _ifUniV3Token0Price(tokenIn, tokenOut); bool _exist; { for (uint256 i = 0; i < feeTypes;){ - address _pool = _getUniV3PoolAddress(token0, token1, univ3_fees[i]); + address _pool = _getUniV3PoolAddress(token0, token1, univ3_fees(i)); if (_pool.isContract()) { _exist = true; break; From 668b8861d64ede918fc57e41c94827733b9978ad Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Wed, 27 Jul 2022 04:13:50 +0200 Subject: [PATCH 31/53] chore: removed TODO from archival contracts --- contracts/OnChainPricingMainnet.sol | 19 +------------------ .../BasicOnChainPricingMainnetLenient.sol | 8 +------- .../archive/FullOnChainPricingMainnet.sol | 5 ----- 3 files changed, 2 insertions(+), 30 deletions(-) diff --git a/contracts/OnChainPricingMainnet.sol b/contracts/OnChainPricingMainnet.sol index 833dd6e..157bde9 100644 --- a/contracts/OnChainPricingMainnet.sol +++ b/contracts/OnChainPricingMainnet.sol @@ -269,23 +269,6 @@ contract OnChainPricingMainnet { // Use dummy magic number as a quick-easy substitute for liquidity (to avoid one SLOAD) since we have pool reserve check in it bool _basicCheck = _checkPoolLiquidityAndBalances(1, (_zeroForOne? _t0Balance : _t1Balance), amountIn); return _basicCheck? getUniV2AmountOutAnalytically(amountIn, (_zeroForOne? _t0Balance : _t1Balance), (_zeroForOne? _t1Balance : _t0Balance)) : 0; - - //address[] memory path = new address[](2); - //path[0] = address(tokenIn); - //path[1] = address(tokenOut); - - //uint256 quote; //0 - - - // TODO: Consider doing check before revert to avoid paying extra gas - // Specifically, test gas if we get revert vs if we check to avoid it - //try IUniswapRouterV2(router).getAmountsOut(amountIn, path) returns (uint256[] memory amounts) { - // quote = amounts[amounts.length - 1]; // Last one is the outToken - //} catch (bytes memory) { - // We ignore as it means it's zero - //} - - //return quote; } /// @dev reference https://etherscan.io/address/0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F#code#L122 @@ -645,7 +628,7 @@ contract OnChainPricingMainnet { /// === UTILS === /// /// @dev Given a address input, return the bytes32 representation - // TODO: Figure out if abi.encode is better + // TODO: Figure out if abi.encode is better -> Benchmark on GasLab function convertToBytes32(address _input) public pure returns (bytes32){ return bytes32(uint256(uint160(_input)) << 96); } diff --git a/contracts/archive/BasicOnChainPricingMainnetLenient.sol b/contracts/archive/BasicOnChainPricingMainnetLenient.sol index 4440f32..91cf4d6 100644 --- a/contracts/archive/BasicOnChainPricingMainnetLenient.sol +++ b/contracts/archive/BasicOnChainPricingMainnetLenient.sol @@ -75,7 +75,7 @@ contract BasicOnChainPricingMainnetLenient { quotes[2] = Quote("sushi", sushiQuote); - /// TODO: Add Balancer and UniV3 + /// NOTE: Lack of Balancer and UniV3 // Because this is a generalized contract, it is best to just loop, @@ -109,8 +109,6 @@ contract BasicOnChainPricingMainnetLenient { uint256 quote; //0 - - // TODO: Consider doing check before revert to avoid paying extra gas // Specifically, test gas if we get revert vs if we check to avoid it try IUniswapRouterV2(router).getAmountsOut(amountIn, path) returns (uint256[] memory amounts) { quote = amounts[amounts.length - 1]; // Last one is the outToken @@ -121,10 +119,6 @@ contract BasicOnChainPricingMainnetLenient { return quote; } - // TODO: Consider adding a `bool` check for `isWeth` to skip the weth check (as it's computed above) - // TODO: Most importantly need to run some gas cost tests to ensure we keep at most at like 120k - - /// @dev Given the address of the CurveLike Router, the input amount, and the path, returns the quote for it function getCurvePrice(address router, address tokenIn, address tokenOut, uint256 amountIn) public view returns (uint256) { (, uint256 curveQuote) = ICurveRouter(router).get_best_rate(tokenIn, tokenOut, amountIn); diff --git a/contracts/archive/FullOnChainPricingMainnet.sol b/contracts/archive/FullOnChainPricingMainnet.sol index 5687110..8adef00 100644 --- a/contracts/archive/FullOnChainPricingMainnet.sol +++ b/contracts/archive/FullOnChainPricingMainnet.sol @@ -209,8 +209,6 @@ contract FullOnChainPricingMainnet { uint256 quote; //0 - - // TODO: Consider doing check before revert to avoid paying extra gas // Specifically, test gas if we get revert vs if we check to avoid it try IUniswapRouterV2(router).getAmountsOut(amountIn, path) returns (uint256[] memory amounts) { quote = amounts[amounts.length - 1]; // Last one is the outToken @@ -424,7 +422,6 @@ contract FullOnChainPricingMainnet { return BALANCERV2_AURA_WETH_POOLID; } else if ((tokenIn == BALWETHBPT && tokenOut == AURABAL) || (tokenOut == BALWETHBPT && tokenIn == AURABAL)){ return BALANCERV2_AURABAL_BALWETH_POOLID; - // TODO CHANGE } else if ((tokenIn == WETH && tokenOut == AURABAL) || (tokenOut == WETH && tokenIn == AURABAL)){ return BALANCERV2_AURABAL_GRAVIAURA_BALWETH_POOLID; } else if ((tokenIn == WETH && tokenOut == GRAVIAURA) || (tokenOut == WETH && tokenIn == GRAVIAURA)){ @@ -444,7 +441,6 @@ contract FullOnChainPricingMainnet { } /// @return assembled curve pools and fees in required Quote struct for given pool - // TODO: Decide if we need fees, as it costs more gas to compute function _getCurveFees(address _pool) internal view returns (bytes32[] memory, uint256[] memory){ bytes32[] memory curvePools = new bytes32[](1); curvePools[0] = convertToBytes32(_pool); @@ -456,7 +452,6 @@ contract FullOnChainPricingMainnet { /// === UTILS === /// /// @dev Given a address input, return the bytes32 representation - // TODO: Figure out if abi.encode is better function convertToBytes32(address _input) public pure returns (bytes32){ return bytes32(uint256(uint160(_input)) << 96); } From 35a8ec581b5e85a9cdceaa0087337e4b47537caf Mon Sep 17 00:00:00 2001 From: rayeaster Date: Wed, 27 Jul 2022 11:19:26 +0800 Subject: [PATCH 32/53] remove redundant tests to speed coverage --- .../heuristic_equivalency.py | 6 +- .../test_bribe_tokens_supported.py | 4 +- .../test_swap_exec_on_chain.py | 18 ++-- tests/on_chain_pricer/test_univ3_pricer.py | 88 +++++++------------ .../on_chain_pricer/test_univ3_pricer_simu.py | 28 ------ 5 files changed, 43 insertions(+), 101 deletions(-) diff --git a/tests/heuristic_equivalency/heuristic_equivalency.py b/tests/heuristic_equivalency/heuristic_equivalency.py index e020253..0f08beb 100644 --- a/tests/heuristic_equivalency/heuristic_equivalency.py +++ b/tests/heuristic_equivalency/heuristic_equivalency.py @@ -14,7 +14,6 @@ """ ### Test findOptimalSwap Equivalencies for different cases - def test_pricing_equivalency_uniswap_v2(weth, pricer, pricer_legacy): token = "0xBC7250C8c3eCA1DfC1728620aF835FCa489bFdf3" # some swap (GM-WETH) only in Uniswap V2 ## 1e18 @@ -126,7 +125,7 @@ def test_pricing_equivalency_uniswap_v3_with_weth(oneE18, wbtc, pricer, pricer_l assert tx2.return_value[0] == 4 ## UNIV3WITHWETH quote_legacy = tx2.return_value[1] - assert quote >= quote_legacy # Optimized quote must be the same or better + assert quote >= quote_legacy # Optimized quote must be the same or better, note the fixed pair in new version of univ3 pricer might cause some nuance there assert tx.gas_used < tx2.gas_used def test_pricing_equivalency_almost_everything(oneE18, wbtc, weth, pricer, pricer_legacy): @@ -145,7 +144,8 @@ def test_pricing_equivalency_almost_everything(oneE18, wbtc, weth, pricer, price assert (tx2.return_value[0] <= 3 or tx2.return_value[0] == 5) ## CURVE or UNIV2 or SUSHI or UNIV3 or BALANCER quote_legacy = tx2.return_value[1] - assert quote >= quote_legacy # Optimized quote must be the same or better + assert tx2.return_value[0] == tx.return_value[0] + assert quote >= quote_legacy # Optimized quote must be the same or better, note the fixed pair in new version of univ3 pricer might cause some nuance there assert tx.gas_used < tx2.gas_used diff --git a/tests/on_chain_pricer/test_bribe_tokens_supported.py b/tests/on_chain_pricer/test_bribe_tokens_supported.py index d48b03a..ea51619 100644 --- a/tests/on_chain_pricer/test_bribe_tokens_supported.py +++ b/tests/on_chain_pricer/test_bribe_tokens_supported.py @@ -32,9 +32,9 @@ TOKENS_18_DECIMALS = [ AURA, AURA_BAL, ## Not Supported -> To FIX TODO ADD BAL POOL - SD, ## Not Supported -> Cannot fix at this time + #SD, ## Not Supported -> Cannot fix at this time DFX, - FDT, ## Not Supported -> Cannot fix at this time + #FDT, ## Not Supported -> Cannot fix at this time LDO, COW, GNO, diff --git a/tests/on_chain_pricer/test_swap_exec_on_chain.py b/tests/on_chain_pricer/test_swap_exec_on_chain.py index e0838be..927d3a0 100644 --- a/tests/on_chain_pricer/test_swap_exec_on_chain.py +++ b/tests/on_chain_pricer/test_swap_exec_on_chain.py @@ -58,15 +58,13 @@ def test_swap_in_univ3_single(oneE18, wbtc_whale, wbtc, usdc, pricer, swapexecut sell_amount = 1 * 100000000 ## minimum quote for WBTC in USDC(1e6) - p = 1 * 15000 * 1000000 - quote = pricer.findOptimalSwap(wbtc.address, usdc.address, sell_amount).return_value - assert quote[1] >= p + p = 1 * 15000 * 1000000 ## swap on chain slippageTolerance = 0.95 wbtc.transfer(swapexecutor.address, sell_amount, {'from': wbtc_whale}) - minOutput = quote[1] * slippageTolerance + minOutput = p * slippageTolerance balBefore = usdc.balanceOf(wbtc_whale) swapexecutor.doOptimalSwapWithQuote(wbtc.address, usdc.address, sell_amount, (3, minOutput, [], [3000]), {'from': wbtc_whale}) balAfter = usdc.balanceOf(wbtc_whale) @@ -82,14 +80,12 @@ def test_swap_in_univ3(oneE18, wbtc_whale, wbtc, weth, usdc, pricer, swapexecuto ## minimum quote for WBTC in USDC(1e6) p = 1 * 15000 * 1000000 - quote = pricer.findOptimalSwap(wbtc.address, usdc.address, sell_amount).return_value - assert quote[1] >= p ## swap on chain slippageTolerance = 0.95 wbtc.transfer(swapexecutor.address, sell_amount, {'from': wbtc_whale}) - minOutput = quote[1] * slippageTolerance + minOutput = p * slippageTolerance ## encodedPath = swapexecutor.encodeUniV3TwoHop(wbtc.address, 500, weth.address, 500, usdc.address) balBefore = usdc.balanceOf(wbtc_whale) swapexecutor.doOptimalSwapWithQuote(wbtc.address, usdc.address, sell_amount, (4, minOutput, [], [500,500]), {'from': wbtc_whale}) @@ -105,14 +101,12 @@ def test_swap_in_balancer_batch(oneE18, wbtc_whale, wbtc, weth, usdc, pricer, sw ## minimum quote for WBTC in USDC(1e6) p = 1 * 15000 * 1000000 - quote = pricer.findOptimalSwap(wbtc.address, usdc.address, sell_amount).return_value - assert quote[1] >= p ## swap on chain slippageTolerance = 0.95 wbtc.transfer(swapexecutor.address, sell_amount, {'from': wbtc_whale}) - minOutput = quote[1] * slippageTolerance + minOutput = p * slippageTolerance wbtc2WETHPoolId = '0xa6f548df93de924d73be7d25dc02554c6bd66db500020000000000000000000e' weth2USDCPoolId = '0x96646936b91d6b9d7d0c47c496afbf3d6ec7b6f8000200000000000000000019' balBefore = usdc.balanceOf(wbtc_whale) @@ -129,14 +123,12 @@ def test_swap_in_balancer_single(oneE18, weth_whale, weth, usdc, pricer, swapexe ## minimum quote for WETH in USDC(1e6) p = 1 * 500 * 1000000 - quote = pricer.findOptimalSwap(weth.address, usdc.address, sell_amount).return_value - assert quote[1] >= p ## swap on chain slippageTolerance = 0.95 weth.transfer(swapexecutor.address, sell_amount, {'from': weth_whale}) - minOutput = quote[1] * slippageTolerance + minOutput = p * slippageTolerance weth2USDCPoolId = '0x96646936b91d6b9d7d0c47c496afbf3d6ec7b6f8000200000000000000000019' balBefore = usdc.balanceOf(weth_whale) swapexecutor.doOptimalSwapWithQuote(weth.address, usdc.address, sell_amount, (5, minOutput, [weth2USDCPoolId], []), {'from': weth_whale}) diff --git a/tests/on_chain_pricer/test_univ3_pricer.py b/tests/on_chain_pricer/test_univ3_pricer.py index eac007e..6af9772 100644 --- a/tests/on_chain_pricer/test_univ3_pricer.py +++ b/tests/on_chain_pricer/test_univ3_pricer.py @@ -1,6 +1,24 @@ import brownie from brownie import * -import pytest +import pytest + +""" + test case for COW token to fix reported issue https://github.com/GalloDaSballo/fair-selling/issues/26 +""" +def test_get_univ3_price_cow(oneE18, weth, usdc_whale, pricer): + ## 1e18 + token = "0xdef1ca1fb7fbcdc777520aa7f396b4e015f497ab" + sell_count = 12209 + sell_amount = sell_count * oneE18 + + ## minimum quote for COW in ETH(1e18) + p = sell_count * 0.00005 * oneE18 + quote = pricer.simulateUniV3Swap(weth.address, sell_amount, token, 10000, False, "0xFCfDFC98062d13a11cec48c44E4613eB26a34293") + assert quote >= p + + ## check against quoter + quoterP = interface.IV3Quoter(pricer.UNIV3_QUOTER()).quoteExactInputSingle.call(token, weth.address, 10000, sell_amount, 0, {'from': usdc_whale.address}) + assert quoterP == quote """ getUniV3Price quote for token A swapped to token B directly: A - > B @@ -11,18 +29,13 @@ def test_get_univ3_price_in_range(oneE18, weth, usdc, usdc_whale, pricer): sell_amount = sell_count * oneE18 ## minimum quote for ETH in USDC(1e6) ## Rip ETH price - p = sell_count * 900 * 1000000 - quote = pricer.sortUniV3Pools(weth.address, sell_amount, usdc.address) - assert quote[0] >= p - quoteInRange = pricer.checkUniV3InRangeLiquidity(usdc.address, weth.address, sell_amount, quote[1], False, "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640") - assert quote[0] == quoteInRange[1] + p = sell_count * 900 * 1000000 + quoteInRange = pricer.checkUniV3InRangeLiquidity(usdc.address, weth.address, sell_amount, 500, False, "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640") + assert quoteInRange[1] >= p ## check against quoter - quoterP = interface.IV3Quoter(pricer.UNIV3_QUOTER()).quoteExactInputSingle(weth.address, usdc.address, quote[1], sell_amount, 0, {'from': usdc_whale.address}).return_value - assert quoterP == quote[0] - - ## fee-0.05% pool is the chosen one among (0.05%, 0.3%, 1%)! - assert quote[1] == 500 + quoterP = interface.IV3Quoter(pricer.UNIV3_QUOTER()).quoteExactInputSingle.call(weth.address, usdc.address, 500, sell_amount, 0, {'from': usdc_whale.address}) + assert quoterP == quoteInRange[1] """ getUniV3Price quote for token A swapped to token B directly: A - > B @@ -33,23 +46,18 @@ def test_get_univ3_price_cross_tick(oneE18, weth, usdc, usdc_whale, pricer): sell_amount = sell_count * oneE18 ## minimum quote for ETH in USDC(1e6) ## Rip ETH price - p = sell_count * 900 * 1000000 - quote = pricer.sortUniV3Pools(weth.address, sell_amount, usdc.address) - assert quote[0] >= p - quoteCrossTicks = pricer.simulateUniV3Swap(usdc.address, sell_amount, weth.address, quote[1], False, "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640") - assert quote[0] == quoteCrossTicks + p = sell_count * 900 * 1000000 + quoteCrossTicks = pricer.simulateUniV3Swap(usdc.address, sell_amount, weth.address, 500, False, "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640") + assert quoteCrossTicks >= p ## check against quoter - quoterP = interface.IV3Quoter(pricer.UNIV3_QUOTER()).quoteExactInputSingle(weth.address, usdc.address, quote[1], sell_amount, 0, {'from': usdc_whale.address}).return_value - assert (abs(quoterP - quote[0]) / quoterP) <= 0.0015 ## thousandsth in quote diff for a millions-dollar-worth swap - - ## fee-0.05% pool is the chosen one among (0.05%, 0.3%, 1%)! - assert quote[1] == 500 + quoterP = interface.IV3Quoter(pricer.UNIV3_QUOTER()).quoteExactInputSingle.call(weth.address, usdc.address, 500, sell_amount, 0, {'from': usdc_whale.address}) + assert (abs(quoterP - quoteCrossTicks) / quoterP) <= 0.0015 ## thousandsth in quote diff for a millions-dollar-worth swap """ getUniV3PriceWithConnector quote for token A swapped to token B with connector token C: A -> C -> B """ -def test_get_univ3_price_with_connector(oneE18, wbtc, usdc, weth, pricer): +def test_get_univ3_price_with_connector(oneE18, wbtc, usdc, weth, dai, pricer): ## 1e8 sell_amount = 100 * 100000000 @@ -59,37 +67,7 @@ def test_get_univ3_price_with_connector(oneE18, wbtc, usdc, weth, pricer): ## min price assert quoteWithConnector >= p - -""" - getUniV3PriceWithConnector quote for stablecoin A swapped to stablecoin B with connector token C: A -> C -> B -""" -def test_get_univ3_price_with_connector_stablecoin(oneE18, dai, usdc, weth, pricer): - ## 1e18 - sell_amount = 10000 * oneE18 - - ## minimum quote for DAI in USDC(1e6) - p = 10000 * 0.99 * 1000000 - quoteWithConnector = pricer.getUniV3PriceWithConnector(dai.address, sell_amount, usdc.address, weth.address) - - ## min price - assert quoteWithConnector >= p - -""" - test case for COW token to fix reported issue https://github.com/GalloDaSballo/fair-selling/issues/26 -""" -def test_get_univ3_price_cow(oneE18, weth, usdc_whale, pricer): - ## 1e18 - token = "0xdef1ca1fb7fbcdc777520aa7f396b4e015f497ab" - sell_count = 12209 - sell_amount = sell_count * oneE18 - - ## minimum quote for COW in ETH(1e18) - p = sell_count * 0.00005 * oneE18 - quote = pricer.sortUniV3Pools(token, sell_amount, weth.address) - print(quote) - assert quote[0] >= p - assert quote[1] == 10000 - ## check against quoter - quoterP = interface.IV3Quoter(pricer.UNIV3_QUOTER()).quoteExactInputSingle.call(token, weth.address, quote[1], sell_amount, 0, {'from': usdc_whale.address}) - assert quoterP == quote[0] \ No newline at end of file + ## test case for stablecoin DAI -> USDC + daiQuoteWithConnector = pricer.getUniV3PriceWithConnector(dai.address, 10000 * oneE18, usdc.address, weth.address) + assert daiQuoteWithConnector >= 10000 * 0.99 * 1000000 diff --git a/tests/on_chain_pricer/test_univ3_pricer_simu.py b/tests/on_chain_pricer/test_univ3_pricer_simu.py index f54c475..57215fe 100644 --- a/tests/on_chain_pricer/test_univ3_pricer_simu.py +++ b/tests/on_chain_pricer/test_univ3_pricer_simu.py @@ -2,34 +2,6 @@ from brownie import * import pytest -""" - simulateUniV3Swap quote for token A swapped to token B directly: A - > B -""" -def test_simu_univ3_swap(oneE18, weth, usdc, pricer): - ## 1e18 - sell_count = 10 - sell_amount = sell_count * oneE18 - - ## minimum quote for ETH in USDC(1e6) ## Rip ETH price - p = sell_count * 900 * 1000000 - quote = pricer.simulateUniV3Swap(usdc.address, sell_amount, weth.address, 500, False, "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640") - - assert quote >= p - -""" - simulateUniV3Swap quote for token A swapped to token B directly: A - > B -""" -def test_simu_univ3_swap2(oneE18, weth, wbtc, pricer): - ## 1e8 - sell_count = 10 - sell_amount = sell_count * 100000000 - - ## minimum quote for BTC in ETH(1e18) ## Rip ETH price - p = sell_count * 14 * oneE18 - quote = pricer.simulateUniV3Swap(wbtc.address, sell_amount, weth.address, 500, True, "0x4585FE77225b41b697C938B018E2Ac67Ac5a20c0") - - assert quote >= p - """ sortUniV3Pools quote for stablecoin A swapped to stablecoin B which try for in-range swap before full-simulation https://info.uniswap.org/#/tokens/0x6b175474e89094c44da98b954eedeac495271d0f From b271cd68475e43f4efd1a32ee32139b138271410 Mon Sep 17 00:00:00 2001 From: rayeaster Date: Thu, 28 Jul 2022 16:14:24 +0800 Subject: [PATCH 33/53] add balance sanity check in balancer pricer --- contracts/OnChainPricingMainnet.sol | 2 + tests/gas_benchmark/benchmark_pricer_gas.py | 2 +- .../test_heuristic_equivalency.py | 176 ------------------ tests/on_chain_pricer/test_balancer_pricer.py | 41 ++-- tests/on_chain_pricer/test_univ3_pricer.py | 7 +- .../on_chain_pricer/test_univ3_pricer_simu.py | 6 +- 6 files changed, 39 insertions(+), 195 deletions(-) delete mode 100644 tests/heuristic_equivalency/test_heuristic_equivalency.py diff --git a/contracts/OnChainPricingMainnet.sol b/contracts/OnChainPricingMainnet.sol index 157bde9..e69e53e 100644 --- a/contracts/OnChainPricingMainnet.sol +++ b/contracts/OnChainPricingMainnet.sol @@ -513,6 +513,8 @@ contract OnChainPricingMainnet { require(_inTokenIdx < tokens.length, '!inBAL'); uint256 _outTokenIdx = _findTokenInBalancePool(tokenOut, tokens); require(_outTokenIdx < tokens.length, '!outBAL'); + + if(balances[_inTokenIdx] <= amountIn) return 0; /// Balancer math for spot price of tokenIn -> tokenOut: weighted value(number * price) relation should be kept try IBalancerV2StablePool(_pool).getAmplificationParameter() returns (uint256 currentAmp, bool isUpdating, uint256 precision) { diff --git a/tests/gas_benchmark/benchmark_pricer_gas.py b/tests/gas_benchmark/benchmark_pricer_gas.py index b4aad55..4aef242 100644 --- a/tests/gas_benchmark/benchmark_pricer_gas.py +++ b/tests/gas_benchmark/benchmark_pricer_gas.py @@ -61,7 +61,7 @@ def test_gas_only_uniswap_v3(oneE18, weth, pricer): tx = pricer.findOptimalSwap(token, weth.address, sell_amount) assert tx.return_value[0] == 3 ## UNIV3 assert tx.return_value[1] > 0 - assert tx.gas_used <= 150000 ## 146254 in test simulation + assert tx.gas_used <= 160000 ## 158204 in test simulation def test_gas_only_uniswap_v3_with_weth(oneE18, wbtc, pricer): token = "0xf4d2888d29D722226FafA5d9B24F9164c092421E" # some swap (LOOKS-WETH-WBTC) only in Uniswap V3 via WETH in between as connector diff --git a/tests/heuristic_equivalency/test_heuristic_equivalency.py b/tests/heuristic_equivalency/test_heuristic_equivalency.py deleted file mode 100644 index e020253..0000000 --- a/tests/heuristic_equivalency/test_heuristic_equivalency.py +++ /dev/null @@ -1,176 +0,0 @@ -from brownie import chain -from rich.console import Console - -console = Console() - -""" - Evaluates the pricing quotes generated by the optimized (heuristic) version of the OnChainPricingMainnet - in contrast to its legacy version. The new version should lead to the same optimal price while consuming - less gas. - - Tests excluded from main test suite as core functionalities are not tested here. In order to add to test - suite, modify the file name to: `test_heuristic_equivalency.py`. Note that tested routes depend on current - liquidity state and, if liquidity moves between protocols, some assertions may fail. -""" - -### Test findOptimalSwap Equivalencies for different cases - -def test_pricing_equivalency_uniswap_v2(weth, pricer, pricer_legacy): - token = "0xBC7250C8c3eCA1DfC1728620aF835FCa489bFdf3" # some swap (GM-WETH) only in Uniswap V2 - ## 1e18 - sell_count = 100000000 - sell_amount = sell_count * 1000000000 ## 1e9 - - chain.snapshot() # To price under same chain conditions (just because) - tx = pricer.findOptimalSwap(token, weth.address, sell_amount) - assert tx.return_value[0] == 1 ## UNIV2 - quote = tx.return_value[1] - - chain.revert() - tx2 = pricer_legacy.findOptimalSwap(token, weth.address, sell_amount) - assert tx2.return_value[0] == 1 ## UNIV2 - quote_legacy = tx2.return_value[1] - - assert quote >= quote_legacy # Optimized quote must be the same or better - assert tx.gas_used < tx2.gas_used - -def test_pricing_equivalency_uniswap_v2_sushi(oneE18, weth, pricer, pricer_legacy): - token = "0x2e9d63788249371f1DFC918a52f8d799F4a38C94" # some swap (TOKE-WETH) only in Uniswap V2 & SushiSwap - ## 1e18 - sell_count = 5000 - sell_amount = sell_count * oneE18 ## 1e18 - - chain.snapshot() # To price under same chain conditions (just because) - tx = pricer.findOptimalSwap(token, weth.address, sell_amount) - assert (tx.return_value[0] == 1 or tx.return_value[0] == 2) ## UNIV2 or SUSHI - quote = tx.return_value[1] - - chain.revert() - tx2 = pricer_legacy.findOptimalSwap(token, weth.address, sell_amount) - assert (tx2.return_value[0] == 1 or tx2.return_value[0] == 2) ## UNIV2 or SUSHI - quote_legacy = tx2.return_value[1] - - assert quote >= quote_legacy # Optimized quote must be the same or better - assert tx.gas_used < tx2.gas_used - -def test_pricing_equivalency_balancer_v2(oneE18, weth, aura, pricer, pricer_legacy): - token = aura # some swap (AURA-WETH) only in Balancer V2 - ## 1e18 - sell_count = 2000 - sell_amount = sell_count * oneE18 ## 1e18 - - chain.snapshot() # To price under same chain conditions (just because) - tx = pricer.findOptimalSwap(token, weth.address, sell_amount) - assert tx.return_value[0] == 5 ## BALANCER - quote = tx.return_value[1] - - chain.revert() - tx2 = pricer_legacy.findOptimalSwap(token, weth.address, sell_amount) - assert tx2.return_value[0] == 5 ## BALANCER - quote_legacy = tx2.return_value[1] - - assert quote >= quote_legacy # Optimized quote must be the same or better - assert tx.gas_used < tx2.gas_used - -def test_pricing_equivalency_balancer_v2_with_weth(oneE18, wbtc, aura, pricer, pricer_legacy): - token = aura # some swap (AURA-WETH-WBTC) only in Balancer V2 via WETH in between as connector - ## 1e18 - sell_count = 2000 - sell_amount = sell_count * oneE18 ## 1e18 - - chain.snapshot() # To price under same chain conditions (just because) - tx = pricer.findOptimalSwap(token, wbtc.address, sell_amount) - assert tx.return_value[0] == 6 ## BALANCERWITHWETH - quote = tx.return_value[1] - - chain.revert() - tx2 = pricer_legacy.findOptimalSwap(token, wbtc.address, sell_amount) - assert tx2.return_value[0] == 6 ## BALANCERWITHWETH - quote_legacy = tx2.return_value[1] - - assert quote >= quote_legacy # Optimized quote must be the same or better - assert tx.gas_used < tx2.gas_used - -def test_pricing_equivalency_uniswap_v3(oneE18, weth, pricer, pricer_legacy): - token = "0xf4d2888d29D722226FafA5d9B24F9164c092421E" # some swap (LOOKS-WETH) only in Uniswap V3 - ## 1e18 - sell_count = 600000 - sell_amount = sell_count * oneE18 ## 1e18 - - chain.snapshot() # To price under same chain conditions (just because) - tx = pricer.findOptimalSwap(token, weth.address, sell_amount) - assert tx.return_value[0] == 3 ## UNIV3 - quote = tx.return_value[1] - - chain.revert() - tx2 = pricer_legacy.findOptimalSwap(token, weth.address, sell_amount) - assert tx2.return_value[0] == 3 ## UNIV3 - quote_legacy = tx2.return_value[1] - - assert quote >= quote_legacy # Optimized quote must be the same or better - assert tx.gas_used < tx2.gas_used - -def test_pricing_equivalency_uniswap_v3_with_weth(oneE18, wbtc, pricer, pricer_legacy): - token = "0xf4d2888d29D722226FafA5d9B24F9164c092421E" # some swap (LOOKS-WETH-WBTC) only in Uniswap V3 via WETH in between as connector - ## 1e18 - sell_count = 600000 - sell_amount = sell_count * oneE18 ## 1e18 - - chain.snapshot() # To price under same chain conditions (just because) - tx = pricer.findOptimalSwap(token, wbtc.address, sell_amount) - assert tx.return_value[0] == 4 ## UNIV3WITHWETH - quote = tx.return_value[1] - - chain.revert() - tx2 = pricer_legacy.findOptimalSwap(token, wbtc.address, sell_amount) - assert tx2.return_value[0] == 4 ## UNIV3WITHWETH - quote_legacy = tx2.return_value[1] - - assert quote >= quote_legacy # Optimized quote must be the same or better - assert tx.gas_used < tx2.gas_used - -def test_pricing_equivalency_almost_everything(oneE18, wbtc, weth, pricer, pricer_legacy): - token = weth # some swap (WETH-WBTC) almost in every DEX, the most gas-consuming scenario - ## 1e18 - sell_count = 10 - sell_amount = sell_count * oneE18 ## 1e18 - - chain.snapshot() # To price under same chain conditions (just because) - tx = pricer.findOptimalSwap(token, wbtc.address, sell_amount) - assert (tx.return_value[0] <= 3 or tx.return_value[0] == 5) ## CURVE or UNIV2 or SUSHI or UNIV3 or BALANCER - quote = tx.return_value[1] - - chain.revert() - tx2 = pricer_legacy.findOptimalSwap(token, wbtc.address, sell_amount) - assert (tx2.return_value[0] <= 3 or tx2.return_value[0] == 5) ## CURVE or UNIV2 or SUSHI or UNIV3 or BALANCER - quote_legacy = tx2.return_value[1] - - assert quote >= quote_legacy # Optimized quote must be the same or better - assert tx.gas_used < tx2.gas_used - - -### Test specific pricing functions for different underlying protocols - -def test_balancer_pricing_equivalency(oneE18, weth, usdc, pricer, pricer_legacy): - ## 1e18 - sell_amount = 1 * oneE18 - - quote = pricer.getBalancerPriceAnalytically(weth.address, sell_amount, usdc.address) - quote_legacy = pricer_legacy.getBalancerPrice(weth.address, sell_amount, usdc.address).return_value - - assert quote >= quote_legacy # Optimized quote must be the same or better - -def test_balancer_pricing_with_connector_equivalency(wbtc, usdc, weth, pricer, pricer_legacy): - ## 1e8 - sell_count = 10 - sell_amount = sell_count * 100000000 - - quote = pricer.getBalancerPriceWithConnectorAnalytically(wbtc.address, sell_amount, usdc.address, weth.address) - quote_legacy = pricer_legacy.getBalancerPriceWithConnector( - wbtc.address, - sell_amount, - usdc.address, - weth.address - ).return_value - - assert quote >= quote_legacy # Optimized quote must be the same or better \ No newline at end of file diff --git a/tests/on_chain_pricer/test_balancer_pricer.py b/tests/on_chain_pricer/test_balancer_pricer.py index 115d64e..4334a9e 100644 --- a/tests/on_chain_pricer/test_balancer_pricer.py +++ b/tests/on_chain_pricer/test_balancer_pricer.py @@ -51,7 +51,10 @@ def test_get_balancer_price_with_connector(oneE18, wbtc, usdc, weth, pricer): ## minimum quote for WBTC in USDC(1e6) p = sell_count * 15000 * 1000000 quote = pricer.getBalancerPriceWithConnectorAnalytically(wbtc.address, sell_amount, usdc.address, weth.address) - assert quote >= p + assert quote >= p + + quoteNotEnoughBalance = pricer.getBalancerPriceWithConnectorAnalytically(wbtc.address, sell_amount * 200, usdc.address, weth.address) + assert quoteNotEnoughBalance == 0 ## price sanity check with dime liquidity #yourCMCKey = 'b527d143-8597-474e-b9b2-5c28c1321c37' @@ -127,22 +130,28 @@ def test_get_balancer_with_connector_no_second_pair(oneE18, balethbpt, badger, w sell_amount = 1000 * oneE18 ## no swap path for BALETHBPT -> WETH -> BADGER in Balancer V2 + quoteNoPool = pricer.getBalancerPriceAnalytically(weth.address, sell_amount, badger.address) + assert quoteNoPool == 0 quoteInRangeAndFee = pricer.getBalancerPriceWithConnectorAnalytically(balethbpt.address, sell_amount, badger.address, weth.address) assert quoteInRangeAndFee == 0 + quoteInRangeAndFee2 = pricer.getBalancerPriceWithConnectorAnalytically(badger.address, sell_amount, "0x2a54ba2964c8cd459dc568853f79813a60761b58", weth.address) + assert quoteInRangeAndFee2 == 0 -def test_get_balancer_pools(weth, pricer): - assert pricer.getBalancerV2Pool(pricer.GRAVIAURA(), weth.address) != pricer.BALANCERV2_NONEXIST_POOLID() ## bveaura - assert pricer.getBalancerV2Pool(pricer.AURA(), weth.address) != pricer.BALANCERV2_NONEXIST_POOLID() - assert pricer.getBalancerV2Pool(pricer.COW(), weth.address) != pricer.BALANCERV2_NONEXIST_POOLID() - assert pricer.getBalancerV2Pool(pricer.COW(), pricer.GNO()) != pricer.BALANCERV2_NONEXIST_POOLID() - assert pricer.getBalancerV2Pool(pricer.OHM(), weth.address) != pricer.BALANCERV2_NONEXIST_POOLID() - assert pricer.getBalancerV2Pool(pricer.AKITA(), weth.address) != pricer.BALANCERV2_NONEXIST_POOLID() - assert pricer.getBalancerV2Pool(pricer.AKITA(), weth.address) != pricer.BALANCERV2_NONEXIST_POOLID() - assert pricer.getBalancerV2Pool(pricer.rETH(), weth.address) != pricer.BALANCERV2_NONEXIST_POOLID() - assert pricer.getBalancerV2Pool(pricer.SRM(), weth.address) != pricer.BALANCERV2_NONEXIST_POOLID() - assert pricer.getBalancerV2Pool(pricer.WSTETH(), weth.address) != pricer.BALANCERV2_NONEXIST_POOLID() - assert pricer.getBalancerV2Pool(pricer.BAL(), weth.address) != pricer.BALANCERV2_NONEXIST_POOLID() - assert pricer.getBalancerV2Pool(pricer.GNO(), weth.address) != pricer.BALANCERV2_NONEXIST_POOLID() - assert pricer.getBalancerV2Pool(pricer.FEI(), weth.address) != pricer.BALANCERV2_NONEXIST_POOLID() - assert pricer.getBalancerV2Pool(pricer.CREAM(), weth.address) != pricer.BALANCERV2_NONEXIST_POOLID() +def test_get_balancer_pools(weth, usdc, pricer): + ## bveaura + nonExistPool = pricer.BALANCERV2_NONEXIST_POOLID() + assert pricer.getBalancerV2Pool(pricer.GRAVIAURA(), weth.address) != nonExistPool and pricer.getBalancerV2Pool(pricer.GRAVIAURA(), usdc.address) == nonExistPool + assert pricer.getBalancerV2Pool(pricer.AURA(), weth.address) != nonExistPool and pricer.getBalancerV2Pool(pricer.AURA(), usdc.address) == nonExistPool + assert pricer.getBalancerV2Pool(pricer.COW(), weth.address) != nonExistPool and pricer.getBalancerV2Pool(pricer.COW(), usdc.address) == nonExistPool + assert pricer.getBalancerV2Pool(pricer.COW(), pricer.GNO()) != nonExistPool + assert pricer.getBalancerV2Pool(pricer.OHM(), weth.address) != nonExistPool and pricer.getBalancerV2Pool(pricer.OHM(), usdc.address) == nonExistPool + assert pricer.getBalancerV2Pool(pricer.AKITA(), weth.address) != nonExistPool and pricer.getBalancerV2Pool(pricer.AKITA(), usdc.address) == nonExistPool + assert pricer.getBalancerV2Pool(pricer.rETH(), weth.address) != nonExistPool and pricer.getBalancerV2Pool(pricer.rETH(), usdc.address) == nonExistPool + assert pricer.getBalancerV2Pool(pricer.SRM(), weth.address) != nonExistPool and pricer.getBalancerV2Pool(pricer.SRM(), usdc.address) == nonExistPool + assert pricer.getBalancerV2Pool(pricer.WSTETH(), weth.address) != nonExistPool and pricer.getBalancerV2Pool(pricer.WSTETH(), usdc.address) == nonExistPool + assert pricer.getBalancerV2Pool(pricer.BAL(), weth.address) != nonExistPool and pricer.getBalancerV2Pool(pricer.BAL(), usdc.address) == nonExistPool + assert pricer.getBalancerV2Pool(pricer.GNO(), weth.address) != nonExistPool and pricer.getBalancerV2Pool(pricer.GNO(), usdc.address) == nonExistPool + assert pricer.getBalancerV2Pool(pricer.FEI(), weth.address) != nonExistPool and pricer.getBalancerV2Pool(pricer.FEI(), usdc.address) == nonExistPool + assert pricer.getBalancerV2Pool(pricer.CREAM(), weth.address) != nonExistPool and pricer.getBalancerV2Pool(pricer.CREAM(), usdc.address) == nonExistPool + assert pricer.getBalancerV2Pool(pricer.WBTC(), pricer.BADGER()) != nonExistPool \ No newline at end of file diff --git a/tests/on_chain_pricer/test_univ3_pricer.py b/tests/on_chain_pricer/test_univ3_pricer.py index 6af9772..f93ee4a 100644 --- a/tests/on_chain_pricer/test_univ3_pricer.py +++ b/tests/on_chain_pricer/test_univ3_pricer.py @@ -12,6 +12,8 @@ def test_get_univ3_price_cow(oneE18, weth, usdc_whale, pricer): sell_amount = sell_count * oneE18 ## minimum quote for COW in ETH(1e18) + quoteInV2 = pricer.getUniPrice(pricer.UNIV2_ROUTER(), weth.address, token, sell_amount) + assert quoteInV2 == 0 p = sell_count * 0.00005 * oneE18 quote = pricer.simulateUniV3Swap(weth.address, sell_amount, token, 10000, False, "0xFCfDFC98062d13a11cec48c44E4613eB26a34293") assert quote >= p @@ -62,7 +64,9 @@ def test_get_univ3_price_with_connector(oneE18, wbtc, usdc, weth, dai, pricer): sell_amount = 100 * 100000000 ## minimum quote for WBTC in USDC(1e6) - p = 100 * 15000 * 1000000 + p = 100 * 15000 * 1000000 + assert pricer.sortUniV3Pools(wbtc.address, sell_amount, usdc.address)[0] >= p + quoteWithConnector = pricer.getUniV3PriceWithConnector(wbtc.address, sell_amount, usdc.address, weth.address) ## min price @@ -71,3 +75,4 @@ def test_get_univ3_price_with_connector(oneE18, wbtc, usdc, weth, dai, pricer): ## test case for stablecoin DAI -> USDC daiQuoteWithConnector = pricer.getUniV3PriceWithConnector(dai.address, 10000 * oneE18, usdc.address, weth.address) assert daiQuoteWithConnector >= 10000 * 0.99 * 1000000 + diff --git a/tests/on_chain_pricer/test_univ3_pricer_simu.py b/tests/on_chain_pricer/test_univ3_pricer_simu.py index 57215fe..2562908 100644 --- a/tests/on_chain_pricer/test_univ3_pricer_simu.py +++ b/tests/on_chain_pricer/test_univ3_pricer_simu.py @@ -80,7 +80,11 @@ def test_only_sushi_support(oneE18, xsushi, usdc, pricer): def test_only_curve_support(oneE18, usdc, pricer): ## 1e18 sell_amount = 1000 * oneE18 - + + ## USDI supported = pricer.isPairSupported("0x2a54ba2964c8cd459dc568853f79813a60761b58", usdc.address, sell_amount) assert supported == True + quoteTx = pricer.findOptimalSwap("0x2a54ba2964c8cd459dc568853f79813a60761b58", usdc.address, sell_amount) + assert quoteTx.return_value[1] > 0 + assert quoteTx.return_value[0] == 0 \ No newline at end of file From aff0b25f4cac6cb026eaa60dcfec9720e32e6a54 Mon Sep 17 00:00:00 2001 From: rayeaster Date: Thu, 28 Jul 2022 16:16:51 +0800 Subject: [PATCH 34/53] restore equivalency base test --- .../{heuristic_equivalency.py => test_heuristic_equivalency.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/heuristic_equivalency/{heuristic_equivalency.py => test_heuristic_equivalency.py} (100%) diff --git a/tests/heuristic_equivalency/heuristic_equivalency.py b/tests/heuristic_equivalency/test_heuristic_equivalency.py similarity index 100% rename from tests/heuristic_equivalency/heuristic_equivalency.py rename to tests/heuristic_equivalency/test_heuristic_equivalency.py From e4a8c40c11a742e4d975d852c9d32c23a1e3e5d2 Mon Sep 17 00:00:00 2001 From: rayeaster Date: Fri, 29 Jul 2022 17:23:47 +0800 Subject: [PATCH 35/53] increase test coverage to over 98% for pricer --- tests/on_chain_pricer/test_balancer_pricer.py | 28 +++++++++++-------- .../on_chain_pricer/test_univ3_pricer_simu.py | 10 +++++++ 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/tests/on_chain_pricer/test_balancer_pricer.py b/tests/on_chain_pricer/test_balancer_pricer.py index 4334a9e..7421947 100644 --- a/tests/on_chain_pricer/test_balancer_pricer.py +++ b/tests/on_chain_pricer/test_balancer_pricer.py @@ -129,29 +129,33 @@ def test_get_balancer_with_connector_no_second_pair(oneE18, balethbpt, badger, w ## 1e18 sell_amount = 1000 * oneE18 - ## no swap path for BALETHBPT -> WETH -> BADGER in Balancer V2 + ## no swap path for WETH -> BADGER in Balancer V2 quoteNoPool = pricer.getBalancerPriceAnalytically(weth.address, sell_amount, badger.address) assert quoteNoPool == 0 - quoteInRangeAndFee = pricer.getBalancerPriceWithConnectorAnalytically(balethbpt.address, sell_amount, badger.address, weth.address) - assert quoteInRangeAndFee == 0 - quoteInRangeAndFee2 = pricer.getBalancerPriceWithConnectorAnalytically(badger.address, sell_amount, "0x2a54ba2964c8cd459dc568853f79813a60761b58", weth.address) - assert quoteInRangeAndFee2 == 0 + ## no swap path for BALETHBPT -> WETH -> BADGER in Balancer V2 + quoteBadger = pricer.getBalancerPriceWithConnectorAnalytically(balethbpt.address, sell_amount, badger.address, weth.address) + assert quoteBadger == 0 + ## no swap path for BADGER -> WBTC -> USDI in Balancer V2 + quoteUSDI = pricer.getBalancerPriceWithConnectorAnalytically(badger.address, sell_amount, "0x2a54ba2964c8cd459dc568853f79813a60761b58", pricer.WBTC()) + assert quoteUSDI == 0 -def test_get_balancer_pools(weth, usdc, pricer): +def test_get_balancer_pools(weth, usdc, wbtc, pricer): ## bveaura nonExistPool = pricer.BALANCERV2_NONEXIST_POOLID() - assert pricer.getBalancerV2Pool(pricer.GRAVIAURA(), weth.address) != nonExistPool and pricer.getBalancerV2Pool(pricer.GRAVIAURA(), usdc.address) == nonExistPool - assert pricer.getBalancerV2Pool(pricer.AURA(), weth.address) != nonExistPool and pricer.getBalancerV2Pool(pricer.AURA(), usdc.address) == nonExistPool + assert pricer.getBalancerV2Pool(pricer.GRAVIAURA(), weth.address) != nonExistPool and pricer.getBalancerV2Pool(pricer.GRAVIAURA(), pricer.USDT()) == nonExistPool + assert pricer.getBalancerV2Pool(pricer.AURA(), weth.address) != nonExistPool and pricer.getBalancerV2Pool(pricer.AURA(), pricer.USDT()) == nonExistPool + assert pricer.getBalancerV2Pool(pricer.AURABAL(), pricer.USDT()) == nonExistPool and pricer.getBalancerV2Pool(pricer.AURABAL(), wbtc.address) == nonExistPool assert pricer.getBalancerV2Pool(pricer.COW(), weth.address) != nonExistPool and pricer.getBalancerV2Pool(pricer.COW(), usdc.address) == nonExistPool - assert pricer.getBalancerV2Pool(pricer.COW(), pricer.GNO()) != nonExistPool + assert pricer.getBalancerV2Pool(pricer.COW(), pricer.GNO()) != nonExistPool and pricer.getBalancerV2Pool(pricer.USDT(), weth.address) == nonExistPool assert pricer.getBalancerV2Pool(pricer.OHM(), weth.address) != nonExistPool and pricer.getBalancerV2Pool(pricer.OHM(), usdc.address) == nonExistPool assert pricer.getBalancerV2Pool(pricer.AKITA(), weth.address) != nonExistPool and pricer.getBalancerV2Pool(pricer.AKITA(), usdc.address) == nonExistPool - assert pricer.getBalancerV2Pool(pricer.rETH(), weth.address) != nonExistPool and pricer.getBalancerV2Pool(pricer.rETH(), usdc.address) == nonExistPool + assert pricer.getBalancerV2Pool(pricer.rETH(), weth.address) != nonExistPool and pricer.getBalancerV2Pool(pricer.rETH(), pricer.USDT()) == nonExistPool assert pricer.getBalancerV2Pool(pricer.SRM(), weth.address) != nonExistPool and pricer.getBalancerV2Pool(pricer.SRM(), usdc.address) == nonExistPool assert pricer.getBalancerV2Pool(pricer.WSTETH(), weth.address) != nonExistPool and pricer.getBalancerV2Pool(pricer.WSTETH(), usdc.address) == nonExistPool - assert pricer.getBalancerV2Pool(pricer.BAL(), weth.address) != nonExistPool and pricer.getBalancerV2Pool(pricer.BAL(), usdc.address) == nonExistPool + assert pricer.getBalancerV2Pool(pricer.BAL(), weth.address) != nonExistPool and pricer.getBalancerV2Pool(pricer.BAL(), pricer.USDT()) == nonExistPool assert pricer.getBalancerV2Pool(pricer.GNO(), weth.address) != nonExistPool and pricer.getBalancerV2Pool(pricer.GNO(), usdc.address) == nonExistPool assert pricer.getBalancerV2Pool(pricer.FEI(), weth.address) != nonExistPool and pricer.getBalancerV2Pool(pricer.FEI(), usdc.address) == nonExistPool assert pricer.getBalancerV2Pool(pricer.CREAM(), weth.address) != nonExistPool and pricer.getBalancerV2Pool(pricer.CREAM(), usdc.address) == nonExistPool - assert pricer.getBalancerV2Pool(pricer.WBTC(), pricer.BADGER()) != nonExistPool + assert pricer.getBalancerV2Pool(pricer.WBTC(), pricer.BADGER()) != nonExistPool and pricer.getBalancerV2Pool(pricer.WBTC(), usdc.address) == nonExistPool + assert pricer.getBalancerV2Pool(pricer.LDO(), weth.address) != nonExistPool and pricer.getBalancerV2Pool(pricer.LDO(), usdc.address) == nonExistPool and pricer.getBalancerV2Pool(pricer.LDO(), wbtc.address) == nonExistPool \ No newline at end of file diff --git a/tests/on_chain_pricer/test_univ3_pricer_simu.py b/tests/on_chain_pricer/test_univ3_pricer_simu.py index 2562908..ff66592 100644 --- a/tests/on_chain_pricer/test_univ3_pricer_simu.py +++ b/tests/on_chain_pricer/test_univ3_pricer_simu.py @@ -30,6 +30,9 @@ def test_simu_univ3_swap_sort_pools_usdt(oneE18, usdt, weth, pricer): assert quoteInRangeAndFee[0] >= p assert quoteInRangeAndFee[1] == 500 ## fee-0.05% pool + quoteSETH2 = pricer.sortUniV3Pools(weth.address, sell_amount, "0xFe2e637202056d30016725477c5da089Ab0A043A") + assert quoteSETH2[0] >= 10 * 0.999 * oneE18 + def test_simu_univ3_swap_usdt_usdc(oneE18, usdt, usdc, pricer): ## 1e18 sell_amount = 10000 * 1000000 @@ -54,6 +57,9 @@ def test_simu_univ3_swap_tusd_usdc(oneE18, tusd, usdc, pricer): assert quoteInRangeAndFee[0] >= p assert quoteInRangeAndFee[1] == 100 ## fee-0.01% pool + quoteUSDM = pricer.sortUniV3Pools(usdc.address, sell_amount, "0xbbAec992fc2d637151dAF40451f160bF85f3C8C1") + assert quoteUSDM[0] >= 10000 * 0.999 * 1000000 + def test_get_univ3_with_connector_no_second_pair(oneE18, balethbpt, usdc, weth, pricer): ## 1e18 sell_amount = 10000 * 1000000 @@ -87,4 +93,8 @@ def test_only_curve_support(oneE18, usdc, pricer): quoteTx = pricer.findOptimalSwap("0x2a54ba2964c8cd459dc568853f79813a60761b58", usdc.address, sell_amount) assert quoteTx.return_value[1] > 0 assert quoteTx.return_value[0] == 0 + + ## not supported yet + isBadgerAuraSupported = pricer.isPairSupported(pricer.BADGER(), pricer.AURA(), sell_amount * 100) + assert isBadgerAuraSupported == False \ No newline at end of file From e6410ee7e78a28baa183654cdfe212e398831858 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Fri, 29 Jul 2022 20:48:13 +0200 Subject: [PATCH 36/53] chore: make tets parallelizable via named imports --- tests/conftest.py | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 867340b..d92cd91 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,13 @@ from time import time from brownie import * +from brownie import ( + accounts, + interface, + UniV3SwapSimulator, + BalancerSwapSimulator, + OnChainPricingMainnetLenient, + OnChainSwapMainnet +) import eth_abi from rich.console import Console import pytest @@ -40,35 +48,35 @@ @pytest.fixture def swapexecutor(): - return OnChainSwapMainnet.deploy({"from": a[0]}) + return OnChainSwapMainnet.deploy({"from": accounts[0]}) @pytest.fixture def pricer(): - univ3simulator = UniV3SwapSimulator.deploy({"from": a[0]}) - balancerV2Simulator = BalancerSwapSimulator.deploy({"from": a[0]}) - return OnChainPricingMainnet.deploy(univ3simulator.address, balancerV2Simulator.address, {"from": a[0]}) + univ3simulator = UniV3SwapSimulator.deploy({"from": accounts[0]}) + balancerV2Simulator = BalancerSwapSimulator.deploy({"from": accounts[0]}) + return OnChainPricingMainnet.deploy(univ3simulator.address, balancerV2Simulator.address, {"from": accounts[0]}) @pytest.fixture def pricer_legacy(): - return FullOnChainPricingMainnet.deploy({"from": a[0]}) + return FullOnChainPricingMainnet.deploy({"from": accounts[0]}) @pytest.fixture def lenient_contract(): ## NOTE: We have 5% slippage on this one - univ3simulator = UniV3SwapSimulator.deploy({"from": a[0]}) - balancerV2Simulator = BalancerSwapSimulator.deploy({"from": a[0]}) - c = OnChainPricingMainnetLenient.deploy(univ3simulator.address, balancerV2Simulator.address, {"from": a[0]}) + univ3simulator = UniV3SwapSimulator.deploy({"from": accounts[0]}) + balancerV2Simulator = BalancerSwapSimulator.deploy({"from": accounts[0]}) + c = OnChainPricingMainnetLenient.deploy(univ3simulator.address, balancerV2Simulator.address, {"from": accounts[0]}) c.setSlippage(499, {"from": accounts.at(c.TECH_OPS(), force=True)}) return c @pytest.fixture def seller(lenient_contract): - return CowSwapDemoSeller.deploy(lenient_contract, {"from": a[0]}) + return CowSwapDemoSeller.deploy(lenient_contract, {"from": accounts[0]}) @pytest.fixture def processor(lenient_contract): - return VotiumBribesProcessor.deploy(lenient_contract, {"from": a[0]}) + return VotiumBribesProcessor.deploy(lenient_contract, {"from": accounts[0]}) @pytest.fixture def oneE18(): @@ -104,7 +112,7 @@ def wbtc(): @pytest.fixture def aura_processor(pricer): - return AuraBribesProcessor.deploy(pricer, {"from": a[0]}) + return AuraBribesProcessor.deploy(pricer, {"from": accounts[0]}) @pytest.fixture def balancer_vault(): From 388e679659ea46cd22197ff69605c8b4337dc053 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Fri, 29 Jul 2022 20:53:22 +0200 Subject: [PATCH 37/53] chore: doc --- contracts/BalancerSwapSimulator.sol | 2 +- contracts/archive/BasicOnChainPricingMainnetLenient.sol | 1 + contracts/archive/FullOnChainPricingMainnet.sol | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/contracts/BalancerSwapSimulator.sol b/contracts/BalancerSwapSimulator.sol index c6792f8..db789fb 100644 --- a/contracts/BalancerSwapSimulator.sol +++ b/contracts/BalancerSwapSimulator.sol @@ -32,7 +32,7 @@ interface IERC20Metadata { /// @dev Swap Simulator for Balancer V2 contract BalancerSwapSimulator { - uint256 internal constant _MAX_IN_RATIO = 0.3e18; + uint256 internal constant _MAX_IN_RATIO = 0.3e18; /// @dev reference https://github.com/balancer-labs/balancer-v2-monorepo/blob/master/pkg/pool-weighted/contracts/WeightedMath.sol#L78 function calcOutGivenIn(ExactInQueryParam memory _query) public view returns (uint256) { diff --git a/contracts/archive/BasicOnChainPricingMainnetLenient.sol b/contracts/archive/BasicOnChainPricingMainnetLenient.sol index 91cf4d6..87c66dd 100644 --- a/contracts/archive/BasicOnChainPricingMainnetLenient.sol +++ b/contracts/archive/BasicOnChainPricingMainnetLenient.sol @@ -15,6 +15,7 @@ import "../../interfaces/curve/ICurveRouter.sol"; /// @title OnChainPricing /// @author Alex the Entreprenerd @ BadgerDAO +/// @dev Pricer V1 /// @dev Mainnet Version of Price Quoter, hardcoded for more efficiency /// @notice To spin a variant, just change the constants and use the Component Functions at the end of the file /// @notice Instead of upgrading in the future, just point to a new implementation diff --git a/contracts/archive/FullOnChainPricingMainnet.sol b/contracts/archive/FullOnChainPricingMainnet.sol index 8adef00..21a50d7 100644 --- a/contracts/archive/FullOnChainPricingMainnet.sol +++ b/contracts/archive/FullOnChainPricingMainnet.sol @@ -28,6 +28,7 @@ enum SwapType { /// @title OnChainPricing /// @author Alex the Entreprenerd for BadgerDAO /// @author Camotelli @rayeaster +/// @dev Pricer V2 /// @dev Mainnet Version of Price Quoter, hardcoded for more efficiency /// @notice Feature Complete, non gas optimized Mainnet Pricer /// A complete quote will cost up to 1.6MLN gas. From e8016171acaede020e50ba49e291d8f279e28826 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Fri, 29 Jul 2022 21:26:50 +0200 Subject: [PATCH 38/53] feat: ignore report of OZ --- brownie-config.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/brownie-config.yml b/brownie-config.yml index f05a560..8192755 100644 --- a/brownie-config.yml +++ b/brownie-config.yml @@ -20,3 +20,6 @@ compiler: reports: exclude_contracts: - SafeERC20 + - IERC20 + - ReentrancyGuard + - Address From 65272ca6702f80f9a18de46f1ba4f04801e97eee Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Fri, 29 Jul 2022 21:27:21 +0200 Subject: [PATCH 39/53] feat: immutable libraries --- contracts/OnChainPricingMainnet.sol | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/contracts/OnChainPricingMainnet.sol b/contracts/OnChainPricingMainnet.sol index e69e53e..5b76fcd 100644 --- a/contracts/OnChainPricingMainnet.sol +++ b/contracts/OnChainPricingMainnet.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.10; @@ -114,14 +114,15 @@ contract OnChainPricingMainnet { uint256 public constant CURVE_FEE_SCALE = 100000; address public constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; - // TODO: Consider making immutable /// @dev helper library to simulate Uniswap V3 swap - address public uniV3Simulator; + address public immutable uniV3Simulator; /// @dev helper library to simulate Balancer V2 swap - address public balancerV2Simulator; + address public immutable balancerV2Simulator; /// UniV3, replaces an array + /// @notice We keep above constructor, because this is a gas optimization + /// Saves storing fee ids in storage, saving 2.1k+ per call uint256 constant univ3_fees_length = 4; function univ3_fees(uint256 i) internal pure returns (uint24) { if(i == 0){ @@ -135,23 +136,10 @@ contract OnChainPricingMainnet { } } - - /// === TEST-ONLY === constructor(address _uniV3Simulator, address _balancerV2Simulator){ uniV3Simulator = _uniV3Simulator; balancerV2Simulator = _balancerV2Simulator; } - - // function setUniV3Simulator(address _uniV3Simulator) external { - // require(_uniV3Simulator != address(0));//TODO permission - // uniV3Simulator = _uniV3Simulator; - // } - - // function setBalancerV2Simulator(address _balancerV2Simulator) external { - // require(_balancerV2Simulator != address(0));//TODO permission - // balancerV2Simulator = _balancerV2Simulator; - // } - /// === END TEST-ONLY === struct Quote { SwapType name; From 3a79dcf7716fa1995f443649e5a7dbc1a3e4be6d Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Fri, 29 Jul 2022 21:27:36 +0200 Subject: [PATCH 40/53] feat: GPL-3 babyyyyy --- README.md | 31 ++++++++++++++----- contracts/AuraBribesProcessor.sol | 2 +- contracts/CowSwapSeller.sol | 19 +++++++++++- contracts/OnChainPricingMainnetLenient.sol | 9 ++++-- contracts/OnChainSwapMainnet.sol | 4 +-- contracts/VotiumBribesProcessor.sol | 2 +- .../BasicOnChainPricingMainnetLenient.sol | 2 +- .../archive/FullOnChainPricingMainnet.sol | 2 +- contracts/demo/CowSwapDemoSeller.sol | 2 +- contracts/demo/TestProcessor.sol | 2 +- contracts/demo/UselessPricer.sol | 2 +- ...quivalency.py => heuristic_equivalency.py} | 0 12 files changed, 57 insertions(+), 20 deletions(-) rename tests/heuristic_equivalency/{test_heuristic_equivalency.py => heuristic_equivalency.py} (100%) diff --git a/README.md b/README.md index 7df27b7..5d2ed27 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,15 @@ # Fair Selling -## Release V0.2 - Pricer - BribesProcessor - CowswapSeller +A BadgerDAO sponsored repo of Open Source Contracts for: +- Integrating Smart Contracts with Cowswap +- Non-Custodial handling of tokens via BribesProcessors +- Calculating onChain Prices +- Executing the best onChain Swap -# CoswapSeller +## Release V0.3 - Pricer - BribesProcessor - CowswapSeller + +# Notable Contracts +## CowswapSeller OnChain Integration with Cowswap, all the functions you want to: - Verify an Order @@ -10,23 +17,33 @@ OnChain Integration with Cowswap, all the functions you want to: - Validate an order through basic security checks (price is correct, sends to correct recipient) - Integrated with an onChain Pricer (see below), to offer stronger execution guarantees -# BribesProcessor +## BribesProcessor -Anti-rug technlogy, allows a Multi-sig to rapidly process cowswap orders, without allowing the Multi to rug +Anti-rug technplogy, allows a Multi-sig to rapidly process CowSwap orders, without allowing the Multi to rug Allows tokens to be rescued without the need for governance via the `ragequit` function -# MainnetPricing +- `AuraBribesProcessor` -> Processor for Votium Bribes earned by `bveAura` +- `VotiumBribesProcessor` -> Processor for Votium Bribes earned by `bveCVX` + +## OnChainPricingMainnet Given a tokenIn, tokenOut and AmountIn, returns a Quote from the most popular dexes -## Dexes Support +- `OnChainPricingMainnet` -> Fully onChain math to find best, single source swap (no fragmented swaps yet) +- `OnChainPricingMainnetLenient` -> Slippage tollerant version of the Pricer + +### Dexes Support - Curve - UniV2 - UniV3 - Balancer - Sushi -Covering >80% TVL on Mainnet. +Covering >80% TVL on Mainnet. (Prob even more) + +# Ar + + ## Example Usage diff --git a/contracts/AuraBribesProcessor.sol b/contracts/AuraBribesProcessor.sol index 6d5caac..77b6fd2 100644 --- a/contracts/AuraBribesProcessor.sol +++ b/contracts/AuraBribesProcessor.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.10; diff --git a/contracts/CowSwapSeller.sol b/contracts/CowSwapSeller.sol index 5024809..bc62107 100644 --- a/contracts/CowSwapSeller.sol +++ b/contracts/CowSwapSeller.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.10; @@ -142,11 +142,15 @@ contract CowSwapSeller is ReentrancyGuard { domainSeparator = SETTLEMENT.domainSeparator(); } + /// @dev Set the Pricer Contract used to determine if a Order is fair + /// @param newPricer - the new pricer function setPricer(OnChainPricing newPricer) external { require(msg.sender == DEV_MULTI); pricer = newPricer; } + /// @dev Set the Manager, the account that can process the tokens + /// @param newManager - the new manager function setManager(address newManager) external { require(msg.sender == manager); manager = newManager; @@ -192,6 +196,9 @@ contract CowSwapSeller is ReentrancyGuard { } } + /// @dev Given the orderData, returns an orderId + /// @param orderData - All the information for a Cowswap Order + /// @return bytes - the OrderId function getOrderID(Data calldata orderData) public view returns (bytes memory) { // Allocated bytes memory orderUid = new bytes(UID_LENGTH); @@ -203,6 +210,13 @@ contract CowSwapSeller is ReentrancyGuard { return orderUid; } + /// @dev Given the orderData and the orderUid + /// Verify the parameter match the id and do basic checks for price and recipient + /// @notice Virtual so you can override, e.g. for Limit Orders by other contracts + /// @notice Reverts on lack of basic validation + /// However it returns false if the slippage check didn't pass + /// Meaning it won't revert if you've been quoted a bad price + /// @return bool - Whether it passed the slippage checks function checkCowswapOrder(Data calldata orderData, bytes memory orderUid) public virtual returns(bool) { // Verify we get the same ID // NOTE: technically superfluous as we could just derive the id and setPresignature with that @@ -232,6 +246,8 @@ contract CowSwapSeller is ReentrancyGuard { /// @dev This is the function you want to use to perform a swap on Cowswap via this smart contract + /// @param orderData - The data for the order, see {Data} + /// @param orderUid - the identifier for the order function _doCowswapOrder(Data calldata orderData, bytes memory orderUid) internal nonReentrant { require(msg.sender == manager); @@ -248,6 +264,7 @@ contract CowSwapSeller is ReentrancyGuard { /// @dev Allows to cancel a cowswap order perhaps if it took too long or was with invalid parameters /// @notice This function performs no checks, there's a high change it will revert if you send it with fluff parameters + /// @param orderUid - The id of the order to cancel function _cancelCowswapOrder(bytes memory orderUid) internal nonReentrant { require(msg.sender == manager); diff --git a/contracts/OnChainPricingMainnetLenient.sol b/contracts/OnChainPricingMainnetLenient.sol index da63abe..4ac27dd 100644 --- a/contracts/OnChainPricingMainnetLenient.sol +++ b/contracts/OnChainPricingMainnetLenient.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.10; @@ -32,8 +32,11 @@ contract OnChainPricingMainnetLenient is OnChainPricingMainnet { uint256 public slippage = 200; // 2% Initially - constructor(address _uniV3Simulator, address _balancerV2Simulator) OnChainPricingMainnet(_uniV3Simulator, _balancerV2Simulator){ - + constructor( + address _uniV3Simulator, + address _balancerV2Simulator + ) OnChainPricingMainnet(_uniV3Simulator, _balancerV2Simulator){ + // Silence is golden } function setSlippage(uint256 newSlippage) external { diff --git a/contracts/OnChainSwapMainnet.sol b/contracts/OnChainSwapMainnet.sol index 30476ec..2a25112 100644 --- a/contracts/OnChainSwapMainnet.sol +++ b/contracts/OnChainSwapMainnet.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.10; import {IERC20} from "@oz/token/ERC20/IERC20.sol"; @@ -45,7 +45,7 @@ contract OnChainSwapMainnet { address public constant SUSHI_ROUTER = 0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F; address public constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; - uint256 public SWAP_SLIPPAGE_TOLERANCE = 500;//initially 5% + uint256 public SWAP_SLIPPAGE_TOLERANCE = 500; // initially 5% uint256 public constant SWAP_SLIPPAGE_MAX = 10000; address public constant TECH_OPS = 0x86cbD0ce0c087b482782c181dA8d191De18C8275; diff --git a/contracts/VotiumBribesProcessor.sol b/contracts/VotiumBribesProcessor.sol index 6a8fca9..d78f78a 100644 --- a/contracts/VotiumBribesProcessor.sol +++ b/contracts/VotiumBribesProcessor.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.10; diff --git a/contracts/archive/BasicOnChainPricingMainnetLenient.sol b/contracts/archive/BasicOnChainPricingMainnetLenient.sol index 87c66dd..634ee85 100644 --- a/contracts/archive/BasicOnChainPricingMainnetLenient.sol +++ b/contracts/archive/BasicOnChainPricingMainnetLenient.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.10; diff --git a/contracts/archive/FullOnChainPricingMainnet.sol b/contracts/archive/FullOnChainPricingMainnet.sol index 21a50d7..f3fa975 100644 --- a/contracts/archive/FullOnChainPricingMainnet.sol +++ b/contracts/archive/FullOnChainPricingMainnet.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.10; diff --git a/contracts/demo/CowSwapDemoSeller.sol b/contracts/demo/CowSwapDemoSeller.sol index c83d46a..ae4e596 100644 --- a/contracts/demo/CowSwapDemoSeller.sol +++ b/contracts/demo/CowSwapDemoSeller.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.10; diff --git a/contracts/demo/TestProcessor.sol b/contracts/demo/TestProcessor.sol index eaffb87..fb37564 100644 --- a/contracts/demo/TestProcessor.sol +++ b/contracts/demo/TestProcessor.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.10; diff --git a/contracts/demo/UselessPricer.sol b/contracts/demo/UselessPricer.sol index ed60dee..de6a265 100644 --- a/contracts/demo/UselessPricer.sol +++ b/contracts/demo/UselessPricer.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity 0.8.10; /// @title OnChainPricing diff --git a/tests/heuristic_equivalency/test_heuristic_equivalency.py b/tests/heuristic_equivalency/heuristic_equivalency.py similarity index 100% rename from tests/heuristic_equivalency/test_heuristic_equivalency.py rename to tests/heuristic_equivalency/heuristic_equivalency.py From 12731ae957a0613720b3079e9c95bf80f9a2f20e Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Fri, 29 Jul 2022 21:36:08 +0200 Subject: [PATCH 41/53] chore: brought back heuristic test --- .../{heuristic_equivalency.py => test_heuristic_equivalency.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/heuristic_equivalency/{heuristic_equivalency.py => test_heuristic_equivalency.py} (100%) diff --git a/tests/heuristic_equivalency/heuristic_equivalency.py b/tests/heuristic_equivalency/test_heuristic_equivalency.py similarity index 100% rename from tests/heuristic_equivalency/heuristic_equivalency.py rename to tests/heuristic_equivalency/test_heuristic_equivalency.py From 617160af68c0eea304e3b2baef72ff4c0c47627c Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Fri, 29 Jul 2022 21:36:16 +0200 Subject: [PATCH 42/53] feat: updated readme --- README.md | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 5d2ed27..de58a21 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,24 @@ # Fair Selling -A BadgerDAO sponsored repo of Open Source Contracts for: +A [BadgerDAO](https://app.badger.com/) sponsored repo of Open Source Contracts for: - Integrating Smart Contracts with Cowswap - Non-Custodial handling of tokens via BribesProcessors - Calculating onChain Prices - Executing the best onChain Swap -## Release V0.3 - Pricer - BribesProcessor - CowswapSeller +## Why bother + +We understand that we cannot prove a optimal price because at any time a new source of liquidity may be available and the contract cannot adapt. + +However we believe that given a set of constraints (available Dexes, handpicked), we can efficiently compute the best trade available to us + +In exploring this issue we aim to: +- Find the most gas-efficient way to get the best executable price (currently 120 /150k per quote, from 1.6MLN) +- Finding the most reliable price we can, to determine if an offer is fair or unfair (Cowswap integration) +- Can we create a "trustless swap" that is provably not frontrun nor manipulated? +- How would such a "self-defending" contract act and how would it be able to defend itself, get the best quote, and be certain of it (with statistical certainty) + +## Current Release V0.3 - Pricer - BribesProcessor - CowswapSeller # Notable Contracts ## CowswapSeller @@ -41,10 +53,6 @@ Given a tokenIn, tokenOut and AmountIn, returns a Quote from the most popular de Covering >80% TVL on Mainnet. (Prob even more) -# Ar - - - ## Example Usage BREAKING CHANGE: V3 is back to `view` even for Balancer and UniV3 functions @@ -88,6 +96,15 @@ Variation of Pricer with a slippage tollerance # Notable Tests +## Proof that the math is accurate with gas savings + +These tests compare the PricerV3 (150k per quote) against V2 (1.6MLN per quote) + +``` +brownie test tests/heuristic_equivalency/test_heuristic_equivalency.py + +``` + ## Benchmark specific AMM quotes TODO: Improve to just use the specific quote From 11cd78e18448254337f4946ac17009ebdc24c949 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Fri, 29 Jul 2022 21:49:10 +0200 Subject: [PATCH 43/53] chore: full named imports --- tests/conftest.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index d92cd91..5b98b31 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,7 +5,12 @@ interface, UniV3SwapSimulator, BalancerSwapSimulator, + OnChainPricingMainnet, + CowSwapDemoSeller, + VotiumBribesProcessor, + AuraBribesProcessor, OnChainPricingMainnetLenient, + FullOnChainPricingMainnet, OnChainSwapMainnet ) import eth_abi From 9942e74e44f60dba61195563a57f50a0b553ad88 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Fri, 29 Jul 2022 21:49:31 +0200 Subject: [PATCH 44/53] chore: removed chain reverts as fork means no state changes --- .../test_heuristic_equivalency.py | 25 ++++--------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/tests/heuristic_equivalency/test_heuristic_equivalency.py b/tests/heuristic_equivalency/test_heuristic_equivalency.py index 0f08beb..97c2772 100644 --- a/tests/heuristic_equivalency/test_heuristic_equivalency.py +++ b/tests/heuristic_equivalency/test_heuristic_equivalency.py @@ -1,4 +1,3 @@ -from brownie import chain from rich.console import Console console = Console() @@ -19,13 +18,11 @@ def test_pricing_equivalency_uniswap_v2(weth, pricer, pricer_legacy): ## 1e18 sell_count = 100000000 sell_amount = sell_count * 1000000000 ## 1e9 - - chain.snapshot() # To price under same chain conditions (just because) + tx = pricer.findOptimalSwap(token, weth.address, sell_amount) assert tx.return_value[0] == 1 ## UNIV2 quote = tx.return_value[1] - chain.revert() tx2 = pricer_legacy.findOptimalSwap(token, weth.address, sell_amount) assert tx2.return_value[0] == 1 ## UNIV2 quote_legacy = tx2.return_value[1] @@ -39,12 +36,10 @@ def test_pricing_equivalency_uniswap_v2_sushi(oneE18, weth, pricer, pricer_legac sell_count = 5000 sell_amount = sell_count * oneE18 ## 1e18 - chain.snapshot() # To price under same chain conditions (just because) tx = pricer.findOptimalSwap(token, weth.address, sell_amount) assert (tx.return_value[0] == 1 or tx.return_value[0] == 2) ## UNIV2 or SUSHI quote = tx.return_value[1] - chain.revert() tx2 = pricer_legacy.findOptimalSwap(token, weth.address, sell_amount) assert (tx2.return_value[0] == 1 or tx2.return_value[0] == 2) ## UNIV2 or SUSHI quote_legacy = tx2.return_value[1] @@ -58,12 +53,10 @@ def test_pricing_equivalency_balancer_v2(oneE18, weth, aura, pricer, pricer_lega sell_count = 2000 sell_amount = sell_count * oneE18 ## 1e18 - chain.snapshot() # To price under same chain conditions (just because) tx = pricer.findOptimalSwap(token, weth.address, sell_amount) assert tx.return_value[0] == 5 ## BALANCER quote = tx.return_value[1] - chain.revert() tx2 = pricer_legacy.findOptimalSwap(token, weth.address, sell_amount) assert tx2.return_value[0] == 5 ## BALANCER quote_legacy = tx2.return_value[1] @@ -76,13 +69,11 @@ def test_pricing_equivalency_balancer_v2_with_weth(oneE18, wbtc, aura, pricer, p ## 1e18 sell_count = 2000 sell_amount = sell_count * oneE18 ## 1e18 - - chain.snapshot() # To price under same chain conditions (just because) + tx = pricer.findOptimalSwap(token, wbtc.address, sell_amount) assert tx.return_value[0] == 6 ## BALANCERWITHWETH quote = tx.return_value[1] - chain.revert() tx2 = pricer_legacy.findOptimalSwap(token, wbtc.address, sell_amount) assert tx2.return_value[0] == 6 ## BALANCERWITHWETH quote_legacy = tx2.return_value[1] @@ -95,13 +86,11 @@ def test_pricing_equivalency_uniswap_v3(oneE18, weth, pricer, pricer_legacy): ## 1e18 sell_count = 600000 sell_amount = sell_count * oneE18 ## 1e18 - - chain.snapshot() # To price under same chain conditions (just because) + tx = pricer.findOptimalSwap(token, weth.address, sell_amount) assert tx.return_value[0] == 3 ## UNIV3 quote = tx.return_value[1] - chain.revert() tx2 = pricer_legacy.findOptimalSwap(token, weth.address, sell_amount) assert tx2.return_value[0] == 3 ## UNIV3 quote_legacy = tx2.return_value[1] @@ -114,13 +103,11 @@ def test_pricing_equivalency_uniswap_v3_with_weth(oneE18, wbtc, pricer, pricer_l ## 1e18 sell_count = 600000 sell_amount = sell_count * oneE18 ## 1e18 - - chain.snapshot() # To price under same chain conditions (just because) + tx = pricer.findOptimalSwap(token, wbtc.address, sell_amount) assert tx.return_value[0] == 4 ## UNIV3WITHWETH quote = tx.return_value[1] - chain.revert() tx2 = pricer_legacy.findOptimalSwap(token, wbtc.address, sell_amount) assert tx2.return_value[0] == 4 ## UNIV3WITHWETH quote_legacy = tx2.return_value[1] @@ -133,13 +120,11 @@ def test_pricing_equivalency_almost_everything(oneE18, wbtc, weth, pricer, price ## 1e18 sell_count = 10 sell_amount = sell_count * oneE18 ## 1e18 - - chain.snapshot() # To price under same chain conditions (just because) + tx = pricer.findOptimalSwap(token, wbtc.address, sell_amount) assert (tx.return_value[0] <= 3 or tx.return_value[0] == 5) ## CURVE or UNIV2 or SUSHI or UNIV3 or BALANCER quote = tx.return_value[1] - chain.revert() tx2 = pricer_legacy.findOptimalSwap(token, wbtc.address, sell_amount) assert (tx2.return_value[0] <= 3 or tx2.return_value[0] == 5) ## CURVE or UNIV2 or SUSHI or UNIV3 or BALANCER quote_legacy = tx2.return_value[1] From 1b4738055cfd9fb43f09611db663bf1d243c6335 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Fri, 29 Jul 2022 21:52:43 +0200 Subject: [PATCH 45/53] feat: deployments --- README.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index de58a21..08765d4 100644 --- a/README.md +++ b/README.md @@ -127,4 +127,15 @@ Run V3 Pricer against V2, to confirm results are correct, but with gas savings ``` brownie test tests/heuristic_equivalency/test_heuristic_equivalency.py -``` \ No newline at end of file +``` + + +# Deployments + +WARNING: This list is not maintained and may be out of date or incorrect. DYOR. + +`bveCVX Bribes Processor`: https://etherscan.io/address/0xb2bf1d48f2c2132913278672e6924efda3385de2 + +`bveAURA Bribes Processor`: https://etherscan.io/address/0x0b6198b324e12a002b60162f8a130d6aedabd04c + +Pricers can be found by checking `processor.pricer()` \ No newline at end of file From 8c3df42cebdc13a51fd9d2acc72a846093593d39 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Fri, 29 Jul 2022 23:49:24 +0200 Subject: [PATCH 46/53] chore: natstep --- contracts/OnChainPricingMainnet.sol | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/contracts/OnChainPricingMainnet.sol b/contracts/OnChainPricingMainnet.sol index 5b76fcd..39c53c0 100644 --- a/contracts/OnChainPricingMainnet.sol +++ b/contracts/OnChainPricingMainnet.sol @@ -183,11 +183,15 @@ contract OnChainPricingMainnet { } /// @dev External function, virtual so you can override, see Lenient Version + /// @param tokenIn - The token you want to sell + /// @param tokenOut - The token you want to buy + /// @param amountIn - The amount of token you want to sell function findOptimalSwap(address tokenIn, address tokenOut, uint256 amountIn) external virtual returns (Quote memory) { return _findOptimalSwap(tokenIn, tokenOut, amountIn); } /// @dev View function for testing the routing of the strategy + /// See {findOptimalSwap} function _findOptimalSwap(address tokenIn, address tokenOut, uint256 amountIn) internal returns (Quote memory) { bool wethInvolved = (tokenIn == WETH || tokenOut == WETH); uint256 length = wethInvolved? 5 : 7; // Add length you need @@ -247,6 +251,7 @@ contract OnChainPricingMainnet { // check pool existence first before quote against it bool _univ2 = (router == UNIV2_ROUTER); + (address _pool, address _token0, address _token1) = pairForUniV2((_univ2? UNIV2_FACTORY : SUSHI_FACTORY), tokenIn, tokenOut, (_univ2? UNIV2_POOL_INITCODE : SUSHI_POOL_INITCODE)); if (!_pool.isContract()){ return 0; From c94984abcba652a50f035c1496b46aca5d242ad8 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Sat, 30 Jul 2022 00:01:16 +0200 Subject: [PATCH 47/53] fix: using `"` for strings for slither --- contracts/BalancerSwapSimulator.sol | 4 ++-- contracts/OnChainPricingMainnet.sol | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/contracts/BalancerSwapSimulator.sol b/contracts/BalancerSwapSimulator.sol index db789fb..928c864 100644 --- a/contracts/BalancerSwapSimulator.sol +++ b/contracts/BalancerSwapSimulator.sol @@ -52,12 +52,12 @@ contract BalancerSwapSimulator { uint256 _scalingFactorIn = _computeScalingFactorWeightedPool(_query.tokenIn); _query.amountIn = BalancerMath.mul(_query.amountIn, _scalingFactorIn); _query.balanceIn = BalancerMath.mul(_query.balanceIn, _scalingFactorIn); - require(_query.balanceIn > _query.amountIn, '!amtIn'); + require(_query.balanceIn > _query.amountIn, "!amtIn"); uint256 _scalingFactorOut = _computeScalingFactorWeightedPool(_query.tokenOut); _query.balanceOut = BalancerMath.mul(_query.balanceOut, _scalingFactorOut); - require(_query.amountIn <= BalancerFixedPoint.mulDown(_query.balanceIn, _MAX_IN_RATIO), '!maxIn'); + require(_query.amountIn <= BalancerFixedPoint.mulDown(_query.balanceIn, _MAX_IN_RATIO), "!maxIn"); uint256 denominator = BalancerFixedPoint.add(_query.balanceIn, _query.amountIn); uint256 base = BalancerFixedPoint.divUp(_query.balanceIn, denominator); diff --git a/contracts/OnChainPricingMainnet.sol b/contracts/OnChainPricingMainnet.sol index 39c53c0..d6e197b 100644 --- a/contracts/OnChainPricingMainnet.sol +++ b/contracts/OnChainPricingMainnet.sol @@ -52,11 +52,11 @@ contract OnChainPricingMainnet { /// == Uni V2 Like Routers || These revert on non-existent pair == // // UniV2 address public constant UNIV2_ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; // Spookyswap - bytes public constant UNIV2_POOL_INITCODE = hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f'; + bytes public constant UNIV2_POOL_INITCODE = hex"96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f"; address public constant UNIV2_FACTORY = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; // Sushi address public constant SUSHI_ROUTER = 0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F; - bytes public constant SUSHI_POOL_INITCODE = hex'e18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303'; + bytes public constant SUSHI_POOL_INITCODE = hex"e18a34eb0e04b04f7a0ac29a6e80748dca96319b42c54d679cb821dca90c6303"; address public constant SUSHI_FACTORY = 0xC0AEe478e3658e2610c5F7A4A2E1777cE9e4f2Ac; // Curve / Doesn't revert on failure @@ -275,7 +275,7 @@ contract OnChainPricingMainnet { function pairForUniV2(address factory, address tokenA, address tokenB, bytes memory _initCode) public pure returns (address, address, address) { (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); address pair = getAddressFromBytes32Lsb(keccak256(abi.encodePacked( - hex'ff', + hex"ff", factory, keccak256(abi.encodePacked(token0, token1)), _initCode // init code hash @@ -503,9 +503,9 @@ contract OnChainPricingMainnet { (address[] memory tokens, uint256[] memory balances, ) = IBalancerV2Vault(BALANCERV2_VAULT).getPoolTokens(poolId); uint256 _inTokenIdx = _findTokenInBalancePool(tokenIn, tokens); - require(_inTokenIdx < tokens.length, '!inBAL'); + require(_inTokenIdx < tokens.length, "!inBAL"); uint256 _outTokenIdx = _findTokenInBalancePool(tokenOut, tokens); - require(_outTokenIdx < tokens.length, '!outBAL'); + require(_outTokenIdx < tokens.length, "!outBAL"); if(balances[_inTokenIdx] <= amountIn) return 0; @@ -520,7 +520,7 @@ contract OnChainPricingMainnet { // weighted pool math { uint256[] memory _weights = IBalancerV2WeightedPool(_pool).getNormalizedWeights(); - require(_weights.length == tokens.length, '!lenBAL'); + require(_weights.length == tokens.length, "!lenBAL"); ExactInQueryParam memory _query = ExactInQueryParam(tokenIn, tokenOut, balances[_inTokenIdx], _weights[_inTokenIdx], balances[_outTokenIdx], _weights[_outTokenIdx], amountIn, IBalancerV2WeightedPool(_pool).getSwapFeePercentage()); _quote = IBalancerV2Simulator(balancerV2Simulator).calcOutGivenIn(_query); } From 76cfe93f30fa1eca495d29e3d218610d3dfbcb4b Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Sat, 30 Jul 2022 00:08:07 +0200 Subject: [PATCH 48/53] chore: explicit false return --- contracts/OnChainPricingMainnet.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/OnChainPricingMainnet.sol b/contracts/OnChainPricingMainnet.sol index d6e197b..9797340 100644 --- a/contracts/OnChainPricingMainnet.sol +++ b/contracts/OnChainPricingMainnet.sol @@ -180,6 +180,8 @@ contract OnChainPricingMainnet { if (curveQuote > 0){ return true; } + + return false; } /// @dev External function, virtual so you can override, see Lenient Version From dd384dc8e1948554b8d6c2e9530cd49b8cf0d3fa Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Sat, 30 Jul 2022 00:09:08 +0200 Subject: [PATCH 49/53] chore: return 1% if nothing else --- contracts/OnChainPricingMainnet.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/OnChainPricingMainnet.sol b/contracts/OnChainPricingMainnet.sol index 9797340..cb4b9d2 100644 --- a/contracts/OnChainPricingMainnet.sol +++ b/contracts/OnChainPricingMainnet.sol @@ -131,9 +131,9 @@ contract OnChainPricingMainnet { return uint24(500); } else if (i == 2) { return uint24(3000); - } else if (i == 3) { - return uint24(10000); - } + } + // else if (i == 3) { + return uint24(10000); } constructor(address _uniV3Simulator, address _balancerV2Simulator){ From ee4b9930d5be91bcb1fa38b0fccb1ee334642d85 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Sat, 30 Jul 2022 00:17:42 +0200 Subject: [PATCH 50/53] feat: removed unused variables --- contracts/OnChainPricingMainnet.sol | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/contracts/OnChainPricingMainnet.sol b/contracts/OnChainPricingMainnet.sol index cb4b9d2..bcb3585 100644 --- a/contracts/OnChainPricingMainnet.sol +++ b/contracts/OnChainPricingMainnet.sol @@ -176,7 +176,7 @@ contract OnChainPricingMainnet { } // Curve at this time has great execution prices but low selection - (address curvePool, uint256 curveQuote) = getCurvePrice(CURVE_ROUTER, tokenIn, tokenOut, amountIn); + (, uint256 curveQuote) = getCurvePrice(CURVE_ROUTER, tokenIn, tokenOut, amountIn); if (curveQuote > 0){ return true; } @@ -254,7 +254,7 @@ contract OnChainPricingMainnet { // check pool existence first before quote against it bool _univ2 = (router == UNIV2_ROUTER); - (address _pool, address _token0, address _token1) = pairForUniV2((_univ2? UNIV2_FACTORY : SUSHI_FACTORY), tokenIn, tokenOut, (_univ2? UNIV2_POOL_INITCODE : SUSHI_POOL_INITCODE)); + (address _pool, address _token0, ) = pairForUniV2((_univ2? UNIV2_FACTORY : SUSHI_FACTORY), tokenIn, tokenOut, (_univ2? UNIV2_POOL_INITCODE : SUSHI_POOL_INITCODE)); if (!_pool.isContract()){ return 0; } @@ -330,7 +330,7 @@ contract OnChainPricingMainnet { // NOTE: A tick is like a ratio, so technically X ticks can offset a fee // Meaning we prob don't need full quote in majority of cases, but can compare number of ticks // per pool per fee and pre-rank based on that - (bool _crossTick, uint256 _outAmt) = _checkSimulationInUniV3(token0, token1, amountIn, _fee, token0Price); + (, uint256 _outAmt) = _checkSimulationInUniV3(token0, token1, amountIn, _fee, token0Price); if (_outAmt > _maxQuote){ _maxQuote = _outAmt; _maxQuoteFee = _fee; @@ -345,7 +345,7 @@ contract OnChainPricingMainnet { /// @dev tell if there exists some Uniswap V3 pool for given token pair function checkUniV3PoolsExistence(address tokenIn, address tokenOut) public view returns (bool){ uint256 feeTypes = univ3_fees_length; - (address token0, address token1, bool token0Price) = _ifUniV3Token0Price(tokenIn, tokenOut); + (address token0, address token1, ) = _ifUniV3Token0Price(tokenIn, tokenOut); bool _exist; { for (uint256 i = 0; i < feeTypes;){ @@ -432,7 +432,7 @@ contract OnChainPricingMainnet { /// @dev Given the address of the input token & amount & the output token /// @return the quote for it function getUniV3Price(address tokenIn, uint256 amountIn, address tokenOut) public view returns (uint256) { - (uint256 _maxInRangeQuote, uint24 _maxPoolFee) = sortUniV3Pools(tokenIn, amountIn, tokenOut); + (uint256 _maxInRangeQuote, ) = sortUniV3Pools(tokenIn, amountIn, tokenOut); return _maxInRangeQuote; } @@ -512,7 +512,7 @@ contract OnChainPricingMainnet { if(balances[_inTokenIdx] <= amountIn) return 0; /// Balancer math for spot price of tokenIn -> tokenOut: weighted value(number * price) relation should be kept - try IBalancerV2StablePool(_pool).getAmplificationParameter() returns (uint256 currentAmp, bool isUpdating, uint256 precision) { + try IBalancerV2StablePool(_pool).getAmplificationParameter() returns (uint256 currentAmp, bool, uint256) { // stable pool math { ExactInStableQueryParam memory _stableQuery = ExactInStableQueryParam(tokens, balances, currentAmp, _inTokenIdx, _outTokenIdx, amountIn, IBalancerV2StablePool(_pool).getSwapFeePercentage()); From bc2c3a30d06905c4abb59b0324e0ce2b0493feb8 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Sat, 30 Jul 2022 00:40:07 +0200 Subject: [PATCH 51/53] feat: pure visibility --- contracts/OnChainPricingMainnet.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/OnChainPricingMainnet.sol b/contracts/OnChainPricingMainnet.sol index bcb3585..3ca7a72 100644 --- a/contracts/OnChainPricingMainnet.sol +++ b/contracts/OnChainPricingMainnet.sol @@ -401,7 +401,7 @@ contract OnChainPricingMainnet { /// @dev internal function for a basic sanity check pool existence and balances /// @return true if basic check pass otherwise false - function _checkPoolLiquidityAndBalances(uint256 _liq, uint256 _reserveIn, uint256 amountIn) internal view returns (bool) { + function _checkPoolLiquidityAndBalances(uint256 _liq, uint256 _reserveIn, uint256 amountIn) internal pure returns (bool) { { // heuristic check0: ensure the pool initiated with valid liquidity in place @@ -557,7 +557,7 @@ contract OnChainPricingMainnet { } /// @return selected BalancerV2 pool given the tokenIn and tokenOut - function getBalancerV2Pool(address tokenIn, address tokenOut) public view returns(bytes32){ + function getBalancerV2Pool(address tokenIn, address tokenOut) public pure returns(bytes32){ (address token0, address token1) = tokenIn < tokenOut ? (tokenIn, tokenOut) : (tokenOut, tokenIn); if (token0 == CREAM && token1 == WETH){ return BALANCERV2_CREAM_WETH_POOLID; From b4915b368350bd51f4c29f2930cd31cf664da670 Mon Sep 17 00:00:00 2001 From: Entreprenerd Date: Sat, 30 Jul 2022 19:04:41 +0200 Subject: [PATCH 52/53] license: https://www.youtube.com/watch?v=PaKIZ7gJlRU&ab_channel=TFiR --- contracts/AuraBribesProcessor.sol | 2 +- contracts/CowSwapSeller.sol | 2 +- contracts/OnChainPricingMainnet.sol | 2 +- contracts/OnChainPricingMainnetLenient.sol | 2 +- contracts/OnChainSwapMainnet.sol | 2 +- contracts/VotiumBribesProcessor.sol | 2 +- contracts/archive/BasicOnChainPricingMainnetLenient.sol | 2 +- contracts/archive/FullOnChainPricingMainnet.sol | 2 +- contracts/demo/CowSwapDemoSeller.sol | 2 +- contracts/demo/TestProcessor.sol | 2 +- contracts/demo/UselessPricer.sol | 2 +- interfaces/balancer/WeightedPoolUserData.sol | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/contracts/AuraBribesProcessor.sol b/contracts/AuraBribesProcessor.sol index 77b6fd2..0770fe3 100644 --- a/contracts/AuraBribesProcessor.sol +++ b/contracts/AuraBribesProcessor.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: GPL-2.0 pragma solidity 0.8.10; diff --git a/contracts/CowSwapSeller.sol b/contracts/CowSwapSeller.sol index bc62107..c227db8 100644 --- a/contracts/CowSwapSeller.sol +++ b/contracts/CowSwapSeller.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: GPL-2.0 pragma solidity 0.8.10; diff --git a/contracts/OnChainPricingMainnet.sol b/contracts/OnChainPricingMainnet.sol index 3ca7a72..bbbbf40 100644 --- a/contracts/OnChainPricingMainnet.sol +++ b/contracts/OnChainPricingMainnet.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: GPL-2.0 pragma solidity 0.8.10; diff --git a/contracts/OnChainPricingMainnetLenient.sol b/contracts/OnChainPricingMainnetLenient.sol index 4ac27dd..4a49328 100644 --- a/contracts/OnChainPricingMainnetLenient.sol +++ b/contracts/OnChainPricingMainnetLenient.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: GPL-2.0 pragma solidity 0.8.10; diff --git a/contracts/OnChainSwapMainnet.sol b/contracts/OnChainSwapMainnet.sol index 2a25112..91b64b0 100644 --- a/contracts/OnChainSwapMainnet.sol +++ b/contracts/OnChainSwapMainnet.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: GPL-2.0 pragma solidity 0.8.10; import {IERC20} from "@oz/token/ERC20/IERC20.sol"; diff --git a/contracts/VotiumBribesProcessor.sol b/contracts/VotiumBribesProcessor.sol index d78f78a..2d8b8cb 100644 --- a/contracts/VotiumBribesProcessor.sol +++ b/contracts/VotiumBribesProcessor.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: GPL-2.0 pragma solidity 0.8.10; diff --git a/contracts/archive/BasicOnChainPricingMainnetLenient.sol b/contracts/archive/BasicOnChainPricingMainnetLenient.sol index 634ee85..f9f3a4c 100644 --- a/contracts/archive/BasicOnChainPricingMainnetLenient.sol +++ b/contracts/archive/BasicOnChainPricingMainnetLenient.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: GPL-2.0 pragma solidity 0.8.10; diff --git a/contracts/archive/FullOnChainPricingMainnet.sol b/contracts/archive/FullOnChainPricingMainnet.sol index f3fa975..a9a8ebe 100644 --- a/contracts/archive/FullOnChainPricingMainnet.sol +++ b/contracts/archive/FullOnChainPricingMainnet.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: GPL-2.0 pragma solidity 0.8.10; diff --git a/contracts/demo/CowSwapDemoSeller.sol b/contracts/demo/CowSwapDemoSeller.sol index ae4e596..5920c3c 100644 --- a/contracts/demo/CowSwapDemoSeller.sol +++ b/contracts/demo/CowSwapDemoSeller.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: GPL-2.0 pragma solidity 0.8.10; diff --git a/contracts/demo/TestProcessor.sol b/contracts/demo/TestProcessor.sol index fb37564..0f4cfdb 100644 --- a/contracts/demo/TestProcessor.sol +++ b/contracts/demo/TestProcessor.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: GPL-2.0 pragma solidity 0.8.10; diff --git a/contracts/demo/UselessPricer.sol b/contracts/demo/UselessPricer.sol index de6a265..dd08c45 100644 --- a/contracts/demo/UselessPricer.sol +++ b/contracts/demo/UselessPricer.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: GPL-2.0 pragma solidity 0.8.10; /// @title OnChainPricing diff --git a/interfaces/balancer/WeightedPoolUserData.sol b/interfaces/balancer/WeightedPoolUserData.sol index c0d967c..bcb2b47 100644 --- a/interfaces/balancer/WeightedPoolUserData.sol +++ b/interfaces/balancer/WeightedPoolUserData.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: GPL-3.0-or-later +// SPDX-License-Identifier: GPL-2.0 // 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 From f23820d8440fbd82959f16e77794eb748bf233d9 Mon Sep 17 00:00:00 2001 From: rayeaster Date: Mon, 1 Aug 2022 17:23:00 +0800 Subject: [PATCH 53/53] make findOptimalSwap view --- contracts/CowSwapSeller.sol | 2 +- contracts/OnChainPricingMainnet.sol | 4 +- contracts/OnChainPricingMainnetLenient.sol | 2 +- contracts/OnChainSwapMainnet.sol | 2 +- contracts/tests/PricerWrapper.sol | 42 +++++++++++ tests/conftest.py | 7 ++ tests/gas_benchmark/benchmark_pricer_gas.py | 67 ++++++++++-------- .../gas_benchmark/benchmark_token_coverage.py | 7 +- .../test_heuristic_equivalency.py | 70 +++++++++++-------- tests/on_chain_pricer/test_balancer_pricer.py | 7 +- .../test_bribe_tokens_supported.py | 5 +- .../on_chain_pricer/test_univ3_pricer_simu.py | 9 +-- 12 files changed, 146 insertions(+), 78 deletions(-) create mode 100644 contracts/tests/PricerWrapper.sol diff --git a/contracts/CowSwapSeller.sol b/contracts/CowSwapSeller.sol index bc62107..4447c56 100644 --- a/contracts/CowSwapSeller.sol +++ b/contracts/CowSwapSeller.sol @@ -29,7 +29,7 @@ struct Quote { uint256[] poolFees; // specific pool fees involved in the optimal swap path, typically in Uniswap V3 } interface OnChainPricing { - function findOptimalSwap(address tokenIn, address tokenOut, uint256 amountIn) external returns (Quote memory); + function findOptimalSwap(address tokenIn, address tokenOut, uint256 amountIn) external view returns (Quote memory); } // END OnchainPricing diff --git a/contracts/OnChainPricingMainnet.sol b/contracts/OnChainPricingMainnet.sol index 3ca7a72..b8a6fe6 100644 --- a/contracts/OnChainPricingMainnet.sol +++ b/contracts/OnChainPricingMainnet.sol @@ -188,13 +188,13 @@ contract OnChainPricingMainnet { /// @param tokenIn - The token you want to sell /// @param tokenOut - The token you want to buy /// @param amountIn - The amount of token you want to sell - function findOptimalSwap(address tokenIn, address tokenOut, uint256 amountIn) external virtual returns (Quote memory) { + function findOptimalSwap(address tokenIn, address tokenOut, uint256 amountIn) external view virtual returns (Quote memory) { return _findOptimalSwap(tokenIn, tokenOut, amountIn); } /// @dev View function for testing the routing of the strategy /// See {findOptimalSwap} - function _findOptimalSwap(address tokenIn, address tokenOut, uint256 amountIn) internal returns (Quote memory) { + function _findOptimalSwap(address tokenIn, address tokenOut, uint256 amountIn) internal view returns (Quote memory) { bool wethInvolved = (tokenIn == WETH || tokenOut == WETH); uint256 length = wethInvolved? 5 : 7; // Add length you need diff --git a/contracts/OnChainPricingMainnetLenient.sol b/contracts/OnChainPricingMainnetLenient.sol index 4ac27dd..b315cc4 100644 --- a/contracts/OnChainPricingMainnetLenient.sol +++ b/contracts/OnChainPricingMainnetLenient.sol @@ -48,7 +48,7 @@ contract OnChainPricingMainnetLenient is OnChainPricingMainnet { // === PRICING === // /// @dev View function for testing the routing of the strategy - function findOptimalSwap(address tokenIn, address tokenOut, uint256 amountIn) external override returns (Quote memory q) { + function findOptimalSwap(address tokenIn, address tokenOut, uint256 amountIn) external view override returns (Quote memory q) { q = _findOptimalSwap(tokenIn, tokenOut, amountIn); q.amountOut = q.amountOut * (MAX_BPS - slippage) / MAX_BPS; } diff --git a/contracts/OnChainSwapMainnet.sol b/contracts/OnChainSwapMainnet.sol index 2a25112..f125f09 100644 --- a/contracts/OnChainSwapMainnet.sol +++ b/contracts/OnChainSwapMainnet.sol @@ -31,7 +31,7 @@ struct Quote { } interface OnChainPricing { - function findOptimalSwap(address tokenIn, address tokenOut, uint256 amountIn) external returns (Quote memory); + function findOptimalSwap(address tokenIn, address tokenOut, uint256 amountIn) external view returns (Quote memory); } /// @dev Mainnet Version of swap for various on-chain dex diff --git a/contracts/tests/PricerWrapper.sol b/contracts/tests/PricerWrapper.sol new file mode 100644 index 0000000..2854367 --- /dev/null +++ b/contracts/tests/PricerWrapper.sol @@ -0,0 +1,42 @@ +pragma solidity 0.8.10; +pragma experimental ABIEncoderV2; + +enum SwapType { + CURVE, //0 + UNIV2, //1 + SUSHI, //2 + UNIV3, //3 + UNIV3WITHWETH, //4 + BALANCER, //5 + BALANCERWITHWETH //6 +} + +// Onchain Pricing Interface +struct Quote { + SwapType name; + uint256 amountOut; + bytes32[] pools; // specific pools involved in the optimal swap path + uint256[] poolFees; // specific pool fees involved in the optimal swap path, typically in Uniswap V3 +} +interface OnChainPricing { + function isPairSupported(address tokenIn, address tokenOut, uint256 amountIn) external view returns (bool); + function findOptimalSwap(address tokenIn, address tokenOut, uint256 amountIn) external view returns (Quote memory); +} +// END OnchainPricing + +contract PricerWrapper { + address public pricer; + constructor(address _pricer) { + pricer = _pricer; + } + + function isPairSupported(address tokenIn, address tokenOut, uint256 amountIn) external view returns (bool) { + return OnChainPricing(pricer).isPairSupported(tokenIn, tokenOut, amountIn); + } + + function findOptimalSwap(address tokenIn, address tokenOut, uint256 amountIn) external view returns (uint256, Quote memory) { + uint256 _gasBefore = gasleft(); + Quote memory q = OnChainPricing(pricer).findOptimalSwap(tokenIn, tokenOut, amountIn); + return (_gasBefore - gasleft(), q); + } +} \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 5b98b31..7c33946 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -55,6 +55,13 @@ def swapexecutor(): return OnChainSwapMainnet.deploy({"from": accounts[0]}) +@pytest.fixture +def pricerwrapper(): + univ3simulator = UniV3SwapSimulator.deploy({"from": accounts[0]}) + balancerV2Simulator = BalancerSwapSimulator.deploy({"from": accounts[0]}) + pricer = OnChainPricingMainnet.deploy(univ3simulator.address, balancerV2Simulator.address, {"from": accounts[0]}) + return PricerWrapper.deploy(pricer.address, {"from": accounts[0]}) + @pytest.fixture def pricer(): univ3simulator = UniV3SwapSimulator.deploy({"from": accounts[0]}) diff --git a/tests/gas_benchmark/benchmark_pricer_gas.py b/tests/gas_benchmark/benchmark_pricer_gas.py index 4aef242..1922774 100644 --- a/tests/gas_benchmark/benchmark_pricer_gas.py +++ b/tests/gas_benchmark/benchmark_pricer_gas.py @@ -8,80 +8,87 @@ Rename the file to test_benchmark_pricer_gas.py to make this part of the testing suite if required """ -def test_gas_only_uniswap_v2(oneE18, weth, pricer): +def test_gas_only_uniswap_v2(oneE18, weth, pricerwrapper): + pricer = pricerwrapper token = "0xBC7250C8c3eCA1DfC1728620aF835FCa489bFdf3" # some swap (GM-WETH) only in Uniswap V2 ## 1e18 sell_count = 100000000 sell_amount = sell_count * 1000000000 ## 1e9 tx = pricer.findOptimalSwap(token, weth.address, sell_amount) - assert tx.return_value[0] == 1 ## UNIV2 - assert tx.return_value[1] > 0 - assert tx.gas_used <= 80000 ## 73925 in test simulation + assert tx[1][0] == 1 ## UNIV2 + assert tx[1][1] > 0 + assert tx[0] <= 80000 ## 73925 in test simulation -def test_gas_uniswap_v2_sushi(oneE18, weth, pricer): +def test_gas_uniswap_v2_sushi(oneE18, weth, pricerwrapper): + pricer = pricerwrapper token = "0x2e9d63788249371f1DFC918a52f8d799F4a38C94" # some swap (TOKE-WETH) only in Uniswap V2 & SushiSwap ## 1e18 sell_count = 5000 sell_amount = sell_count * oneE18 ## 1e18 tx = pricer.findOptimalSwap(token, weth.address, sell_amount) - assert (tx.return_value[0] == 1 or tx.return_value[0] == 2) ## UNIV2 or SUSHI - assert tx.return_value[1] > 0 - assert tx.gas_used <= 90000 ## 83158 in test simulation + assert (tx[1][0] == 1 or tx[1][0] == 2) ## UNIV2 or SUSHI + assert tx[1][1] > 0 + assert tx[0] <= 90000 ## 83158 in test simulation -def test_gas_only_balancer_v2(oneE18, weth, aura, pricer): +def test_gas_only_balancer_v2(oneE18, weth, aura, pricerwrapper): + pricer = pricerwrapper token = aura # some swap (AURA-WETH) only in Balancer V2 ## 1e18 - sell_count = 2000 + sell_count = 8000 sell_amount = sell_count * oneE18 ## 1e18 tx = pricer.findOptimalSwap(token, weth.address, sell_amount) - assert tx.return_value[0] == 5 ## BALANCER - assert tx.return_value[1] > 0 - assert tx.gas_used <= 110000 ## 101190 in test simulation + assert tx[1][0] == 5 ## BALANCER + assert tx[1][1] > 0 + assert tx[0] <= 110000 ## 101190 in test simulation -def test_gas_only_balancer_v2_with_weth(oneE18, wbtc, aura, pricer): +def test_gas_only_balancer_v2_with_weth(oneE18, wbtc, aura, pricerwrapper): + pricer = pricerwrapper token = aura # some swap (AURA-WETH-WBTC) only in Balancer V2 via WETH in between as connector ## 1e18 - sell_count = 2000 + sell_count = 8000 sell_amount = sell_count * oneE18 ## 1e18 tx = pricer.findOptimalSwap(token, wbtc.address, sell_amount) - assert tx.return_value[0] == 6 ## BALANCERWITHWETH - assert tx.return_value[1] > 0 - assert tx.gas_used <= 170000 ## 161690 in test simulation + assert tx[1][0] == 6 ## BALANCERWITHWETH + assert tx[1][1] > 0 + assert tx[0] <= 170000 ## 161690 in test simulation -def test_gas_only_uniswap_v3(oneE18, weth, pricer): +def test_gas_only_uniswap_v3(oneE18, weth, pricerwrapper): + pricer = pricerwrapper token = "0xf4d2888d29D722226FafA5d9B24F9164c092421E" # some swap (LOOKS-WETH) only in Uniswap V3 ## 1e18 sell_count = 600000 sell_amount = sell_count * oneE18 ## 1e18 tx = pricer.findOptimalSwap(token, weth.address, sell_amount) - assert tx.return_value[0] == 3 ## UNIV3 - assert tx.return_value[1] > 0 - assert tx.gas_used <= 160000 ## 158204 in test simulation + assert tx[1][0] == 3 ## UNIV3 + assert tx[1][1] > 0 + assert tx[0] <= 160000 ## 158204 in test simulation -def test_gas_only_uniswap_v3_with_weth(oneE18, wbtc, pricer): +def test_gas_only_uniswap_v3_with_weth(oneE18, wbtc, pricerwrapper): + pricer = pricerwrapper token = "0xf4d2888d29D722226FafA5d9B24F9164c092421E" # some swap (LOOKS-WETH-WBTC) only in Uniswap V3 via WETH in between as connector ## 1e18 sell_count = 600000 sell_amount = sell_count * oneE18 ## 1e18 tx = pricer.findOptimalSwap(token, wbtc.address, sell_amount) - assert tx.return_value[0] == 4 ## UNIV3WITHWETH - assert tx.return_value[1] > 0 - assert tx.gas_used <= 230000 ## 227498 in test simulation + assert tx[1][0] == 4 ## UNIV3WITHWETH + assert tx[1][1] > 0 + assert tx[0] <= 230000 ## 227498 in test simulation -def test_gas_almost_everything(oneE18, wbtc, weth, pricer): +def test_gas_almost_everything(oneE18, wbtc, weth, pricerwrapper): + pricer = pricerwrapper token = weth # some swap (WETH-WBTC) almost in every DEX, the most gas-consuming scenario ## 1e18 sell_count = 10 sell_amount = sell_count * oneE18 ## 1e18 tx = pricer.findOptimalSwap(token, wbtc.address, sell_amount) - assert (tx.return_value[0] <= 3 or tx.return_value[0] == 5) ## CURVE or UNIV2 or SUSHI or UNIV3 or BALANCER - assert tx.return_value[1] > 0 - assert tx.gas_used <= 210000 ## 200229 in test simulation + assert (tx[1][0] <= 3 or tx[1][0] == 5) ## CURVE or UNIV2 or SUSHI or UNIV3 or BALANCER + assert tx[1][1] > 0 + assert tx[0] <= 210000 ## 200229 in test simulation \ No newline at end of file diff --git a/tests/gas_benchmark/benchmark_token_coverage.py b/tests/gas_benchmark/benchmark_token_coverage.py index c527522..6e3a300 100644 --- a/tests/gas_benchmark/benchmark_token_coverage.py +++ b/tests/gas_benchmark/benchmark_token_coverage.py @@ -48,12 +48,13 @@ ] @pytest.mark.parametrize("token,count", TOP_DECIMAL18_TOKENS) -def test_token_decimal18(oneE18, weth, token, count, pricer): +def test_token_decimal18(oneE18, weth, token, count, pricerwrapper): + pricer = pricerwrapper sell_token = token ## 1e18 sell_count = count sell_amount = sell_count * oneE18 ## 1e18 - quote = pricer.findOptimalSwap.call(sell_token, weth.address, sell_amount) - assert quote[1] > 0 + quote = pricer.findOptimalSwap(sell_token, weth.address, sell_amount) + assert quote[1][1] > 0 \ No newline at end of file diff --git a/tests/heuristic_equivalency/test_heuristic_equivalency.py b/tests/heuristic_equivalency/test_heuristic_equivalency.py index 97c2772..d458f5b 100644 --- a/tests/heuristic_equivalency/test_heuristic_equivalency.py +++ b/tests/heuristic_equivalency/test_heuristic_equivalency.py @@ -1,5 +1,6 @@ from rich.console import Console +import pytest console = Console() """ @@ -13,125 +14,132 @@ """ ### Test findOptimalSwap Equivalencies for different cases -def test_pricing_equivalency_uniswap_v2(weth, pricer, pricer_legacy): +def test_pricing_equivalency_uniswap_v2(weth, pricerwrapper, pricer_legacy): + pricer = pricerwrapper token = "0xBC7250C8c3eCA1DfC1728620aF835FCa489bFdf3" # some swap (GM-WETH) only in Uniswap V2 ## 1e18 sell_count = 100000000 sell_amount = sell_count * 1000000000 ## 1e9 tx = pricer.findOptimalSwap(token, weth.address, sell_amount) - assert tx.return_value[0] == 1 ## UNIV2 - quote = tx.return_value[1] + assert tx[1][0] == 1 ## UNIV2 + quote = tx[1][1] tx2 = pricer_legacy.findOptimalSwap(token, weth.address, sell_amount) assert tx2.return_value[0] == 1 ## UNIV2 quote_legacy = tx2.return_value[1] assert quote >= quote_legacy # Optimized quote must be the same or better - assert tx.gas_used < tx2.gas_used + assert tx[0] > 0 and tx[0] < tx2.gas_used -def test_pricing_equivalency_uniswap_v2_sushi(oneE18, weth, pricer, pricer_legacy): +def test_pricing_equivalency_uniswap_v2_sushi(oneE18, weth, pricerwrapper, pricer_legacy): + pricer = pricerwrapper token = "0x2e9d63788249371f1DFC918a52f8d799F4a38C94" # some swap (TOKE-WETH) only in Uniswap V2 & SushiSwap ## 1e18 sell_count = 5000 sell_amount = sell_count * oneE18 ## 1e18 tx = pricer.findOptimalSwap(token, weth.address, sell_amount) - assert (tx.return_value[0] == 1 or tx.return_value[0] == 2) ## UNIV2 or SUSHI - quote = tx.return_value[1] + assert (tx[1][0] == 1 or tx[1][0] == 2) ## UNIV2 or SUSHI + quote = tx[1][1] tx2 = pricer_legacy.findOptimalSwap(token, weth.address, sell_amount) assert (tx2.return_value[0] == 1 or tx2.return_value[0] == 2) ## UNIV2 or SUSHI quote_legacy = tx2.return_value[1] assert quote >= quote_legacy # Optimized quote must be the same or better - assert tx.gas_used < tx2.gas_used + assert tx[0] > 0 and tx[0] < tx2.gas_used -def test_pricing_equivalency_balancer_v2(oneE18, weth, aura, pricer, pricer_legacy): +def test_pricing_equivalency_balancer_v2(oneE18, weth, aura, pricerwrapper, pricer_legacy): + pricer = pricerwrapper token = aura # some swap (AURA-WETH) only in Balancer V2 ## 1e18 - sell_count = 2000 + sell_count = 8000 sell_amount = sell_count * oneE18 ## 1e18 tx = pricer.findOptimalSwap(token, weth.address, sell_amount) - assert tx.return_value[0] == 5 ## BALANCER - quote = tx.return_value[1] + assert tx[1][0] == 5 ## BALANCER + quote = tx[1][1] tx2 = pricer_legacy.findOptimalSwap(token, weth.address, sell_amount) assert tx2.return_value[0] == 5 ## BALANCER quote_legacy = tx2.return_value[1] assert quote >= quote_legacy # Optimized quote must be the same or better - assert tx.gas_used < tx2.gas_used + assert tx[0] > 0 and tx[0] < tx2.gas_used -def test_pricing_equivalency_balancer_v2_with_weth(oneE18, wbtc, aura, pricer, pricer_legacy): +def test_pricing_equivalency_balancer_v2_with_weth(oneE18, wbtc, aura, pricerwrapper, pricer_legacy): + pricer = pricerwrapper token = aura # some swap (AURA-WETH-WBTC) only in Balancer V2 via WETH in between as connector ## 1e18 - sell_count = 2000 + sell_count = 8000 sell_amount = sell_count * oneE18 ## 1e18 tx = pricer.findOptimalSwap(token, wbtc.address, sell_amount) - assert tx.return_value[0] == 6 ## BALANCERWITHWETH - quote = tx.return_value[1] + assert tx[1][0] == 6 ## BALANCERWITHWETH + quote = tx[1][1] tx2 = pricer_legacy.findOptimalSwap(token, wbtc.address, sell_amount) assert tx2.return_value[0] == 6 ## BALANCERWITHWETH quote_legacy = tx2.return_value[1] assert quote >= quote_legacy # Optimized quote must be the same or better - assert tx.gas_used < tx2.gas_used + assert tx[0] > 0 and tx[0] < tx2.gas_used -def test_pricing_equivalency_uniswap_v3(oneE18, weth, pricer, pricer_legacy): +def test_pricing_equivalency_uniswap_v3(oneE18, weth, pricerwrapper, pricer_legacy): + pricer = pricerwrapper token = "0xf4d2888d29D722226FafA5d9B24F9164c092421E" # some swap (LOOKS-WETH) only in Uniswap V3 ## 1e18 sell_count = 600000 sell_amount = sell_count * oneE18 ## 1e18 tx = pricer.findOptimalSwap(token, weth.address, sell_amount) - assert tx.return_value[0] == 3 ## UNIV3 - quote = tx.return_value[1] + assert tx[1][0] == 3 ## UNIV3 + quote = tx[1][1] tx2 = pricer_legacy.findOptimalSwap(token, weth.address, sell_amount) assert tx2.return_value[0] == 3 ## UNIV3 quote_legacy = tx2.return_value[1] assert quote >= quote_legacy # Optimized quote must be the same or better - assert tx.gas_used < tx2.gas_used + assert tx[0] > 0 and tx[0] < tx2.gas_used -def test_pricing_equivalency_uniswap_v3_with_weth(oneE18, wbtc, pricer, pricer_legacy): +def test_pricing_equivalency_uniswap_v3_with_weth(oneE18, wbtc, pricerwrapper, pricer_legacy): + pricer = pricerwrapper token = "0xf4d2888d29D722226FafA5d9B24F9164c092421E" # some swap (LOOKS-WETH-WBTC) only in Uniswap V3 via WETH in between as connector ## 1e18 sell_count = 600000 sell_amount = sell_count * oneE18 ## 1e18 tx = pricer.findOptimalSwap(token, wbtc.address, sell_amount) - assert tx.return_value[0] == 4 ## UNIV3WITHWETH - quote = tx.return_value[1] + assert tx[1][0] == 4 ## UNIV3WITHWETH + quote = tx[1][1] tx2 = pricer_legacy.findOptimalSwap(token, wbtc.address, sell_amount) assert tx2.return_value[0] == 4 ## UNIV3WITHWETH quote_legacy = tx2.return_value[1] assert quote >= quote_legacy # Optimized quote must be the same or better, note the fixed pair in new version of univ3 pricer might cause some nuance there - assert tx.gas_used < tx2.gas_used + assert tx[0] > 0 and tx[0] < tx2.gas_used -def test_pricing_equivalency_almost_everything(oneE18, wbtc, weth, pricer, pricer_legacy): +def test_pricing_equivalency_almost_everything(oneE18, wbtc, weth, pricerwrapper, pricer_legacy): + pricer = pricerwrapper token = weth # some swap (WETH-WBTC) almost in every DEX, the most gas-consuming scenario ## 1e18 sell_count = 10 sell_amount = sell_count * oneE18 ## 1e18 tx = pricer.findOptimalSwap(token, wbtc.address, sell_amount) - assert (tx.return_value[0] <= 3 or tx.return_value[0] == 5) ## CURVE or UNIV2 or SUSHI or UNIV3 or BALANCER - quote = tx.return_value[1] + assert (tx[1][0] <= 3 or tx[1][0] == 5) ## CURVE or UNIV2 or SUSHI or UNIV3 or BALANCER + quote = tx[1][1] tx2 = pricer_legacy.findOptimalSwap(token, wbtc.address, sell_amount) assert (tx2.return_value[0] <= 3 or tx2.return_value[0] == 5) ## CURVE or UNIV2 or SUSHI or UNIV3 or BALANCER quote_legacy = tx2.return_value[1] - assert tx2.return_value[0] == tx.return_value[0] + assert tx2.return_value[0] == tx[1][0] assert quote >= quote_legacy # Optimized quote must be the same or better, note the fixed pair in new version of univ3 pricer might cause some nuance there - assert tx.gas_used < tx2.gas_used + assert tx[0] > 0 and tx[0] < tx2.gas_used ### Test specific pricing functions for different underlying protocols diff --git a/tests/on_chain_pricer/test_balancer_pricer.py b/tests/on_chain_pricer/test_balancer_pricer.py index 7421947..11718f7 100644 --- a/tests/on_chain_pricer/test_balancer_pricer.py +++ b/tests/on_chain_pricer/test_balancer_pricer.py @@ -105,7 +105,8 @@ def test_get_balancer_price_aurabal_analytical(oneE18, aurabal, weth, pricer): """ getBalancerPrice quote for token A swapped to token B directly using given balancer pool: A - > B """ -def test_get_balancer_price_aurabal_bpt_analytical(oneE18, aurabal, balethbpt, pricer): +def test_get_balancer_price_aurabal_bpt_analytical(oneE18, aurabal, balethbpt, pricerwrapper): + pricer = pricerwrapper ## 1e18 sell_count = 100 sell_amount = sell_count * oneE18 @@ -114,8 +115,8 @@ def test_get_balancer_price_aurabal_bpt_analytical(oneE18, aurabal, balethbpt, p p = sell_count * 1 * oneE18 ## there is a proper pool in Balancer for AURABAL in BAL-ETH bpt - quote = pricer.findOptimalSwap(balethbpt.address, aurabal.address, sell_amount).return_value - assert quote[1] >= p + quote = pricer.findOptimalSwap(balethbpt.address, aurabal.address, sell_amount) + assert quote[1][1] >= p def test_balancer_not_supported_tokens(oneE18, tusd, usdc, pricer): ## tokenIn not in the given balancer pool diff --git a/tests/on_chain_pricer/test_bribe_tokens_supported.py b/tests/on_chain_pricer/test_bribe_tokens_supported.py index ea51619..c2d23af 100644 --- a/tests/on_chain_pricer/test_bribe_tokens_supported.py +++ b/tests/on_chain_pricer/test_bribe_tokens_supported.py @@ -47,7 +47,8 @@ ] @pytest.mark.parametrize("token", TOKENS_18_DECIMALS) -def test_are_bribes_supported(pricer, token): +def test_are_bribes_supported(pricerwrapper, token): + pricer = pricerwrapper """ Given a bunch of tokens historically used as bribes, verifies the pricer will return non-zero value We sell all to WETH which is pretty realistic @@ -60,5 +61,5 @@ def test_are_bribes_supported(pricer, token): assert res quote = pricer.findOptimalSwap.call(token, WETH, AMOUNT) - assert quote[1] > 0 + assert quote[1][1] > 0 diff --git a/tests/on_chain_pricer/test_univ3_pricer_simu.py b/tests/on_chain_pricer/test_univ3_pricer_simu.py index ff66592..5193dc6 100644 --- a/tests/on_chain_pricer/test_univ3_pricer_simu.py +++ b/tests/on_chain_pricer/test_univ3_pricer_simu.py @@ -83,7 +83,8 @@ def test_only_sushi_support(oneE18, xsushi, usdc, pricer): supported = pricer.isPairSupported(xsushi.address, usdc.address, sell_amount) assert supported == True -def test_only_curve_support(oneE18, usdc, pricer): +def test_only_curve_support(oneE18, usdc, badger, aura, pricerwrapper): + pricer = pricerwrapper ## 1e18 sell_amount = 1000 * oneE18 @@ -91,10 +92,10 @@ def test_only_curve_support(oneE18, usdc, pricer): supported = pricer.isPairSupported("0x2a54ba2964c8cd459dc568853f79813a60761b58", usdc.address, sell_amount) assert supported == True quoteTx = pricer.findOptimalSwap("0x2a54ba2964c8cd459dc568853f79813a60761b58", usdc.address, sell_amount) - assert quoteTx.return_value[1] > 0 - assert quoteTx.return_value[0] == 0 + assert quoteTx[1][1] > 0 + assert quoteTx[1][0] == 0 ## not supported yet - isBadgerAuraSupported = pricer.isPairSupported(pricer.BADGER(), pricer.AURA(), sell_amount * 100) + isBadgerAuraSupported = pricer.isPairSupported(badger.address, aura.address, sell_amount * 100) assert isBadgerAuraSupported == False \ No newline at end of file