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/README.md b/README.md index 347361c..08765d4 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,27 @@ # Fair Selling -## Release V0.2 - Pricer - BribesProcessor - CowswapSeller +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 -# CoswapSeller +## 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 OnChain Integration with Cowswap, all the functions you want to: - Verify an Order @@ -10,27 +29,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) ## 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 +70,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 +84,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) ``` @@ -67,3 +92,50 @@ quote = pricer.findOptimalSwap.call(t_in, t_out, amt_in) ## Add .call to avoid p 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 + +``` +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 +``` + + +# 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 diff --git a/brownie-config.yml b/brownie-config.yml index 4b6462b..8192755 100644 --- a/brownie-config.yml +++ b/brownie-config.yml @@ -13,10 +13,13 @@ 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/" reports: exclude_contracts: - SafeERC20 + - IERC20 + - ReentrancyGuard + - Address diff --git a/contracts/AuraBribesProcessor.sol b/contracts/AuraBribesProcessor.sol index 6d5caac..0770fe3 100644 --- a/contracts/AuraBribesProcessor.sol +++ b/contracts/AuraBribesProcessor.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0 pragma solidity 0.8.10; diff --git a/contracts/BalancerSwapSimulator.sol b/contracts/BalancerSwapSimulator.sol new file mode 100644 index 0000000..928c864 --- /dev/null +++ b/contracts/BalancerSwapSimulator.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: MIT +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; + + /// @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) { + /********************************************************************************************** + // outGivenIn // + // aO = amountOut // + // bO = balanceOut // + // bI = balanceIn / / bI \ (wI / wO) \ // + // aI = amountIn aO = bO * | 1 - | -------------------------- | ^ | // + // wI = weightIn \ \ ( bI + aI ) / / // + // wO = weightOut // + **********************************************************************************************/ + + // 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); + + 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/CowSwapSeller.sol b/contracts/CowSwapSeller.sol index 5024809..3e643da 100644 --- a/contracts/CowSwapSeller.sol +++ b/contracts/CowSwapSeller.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0 pragma solidity 0.8.10; @@ -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 @@ -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/OnChainPricingMainnet.sol b/contracts/OnChainPricingMainnet.sol index 13db1e9..6ea2cea 100644 --- a/contracts/OnChainPricingMainnet.sol +++ b/contracts/OnChainPricingMainnet.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0 pragma solidity 0.8.10; @@ -7,13 +7,17 @@ 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/IV2Pool.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"; +import "../interfaces/balancer/IBalancerV2Simulator.sol"; enum SwapType { CURVE, //0 @@ -48,8 +52,12 @@ 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"; + 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 @@ -58,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"; @@ -99,12 +106,40 @@ 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; 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 immutable uniV3Simulator; + /// @dev helper library to simulate Balancer V2 swap + 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){ + return uint24(100); + } else if (i == 1) { + return uint24(500); + } else if (i == 2) { + return uint24(3000); + } + // else if (i == 3) { + return uint24(10000); + } + + constructor(address _uniV3Simulator, address _balancerV2Simulator){ + uniV3Simulator = _uniV3Simulator; + balancerV2Simulator = _balancerV2Simulator; + } struct Quote { SwapType name; @@ -115,7 +150,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 @@ -126,7 +161,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; } @@ -141,19 +176,25 @@ 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; } + + return false; } /// @dev External function, virtual so you can override, see Lenient Version - function findOptimalSwap(address tokenIn, address tokenOut, uint256 amountIn) external virtual returns (Quote memory) { + /// @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 view 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) { + /// See {findOptimalSwap} + 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 @@ -175,12 +216,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[5] = Quote(SwapType.UNIV3WITHWETH, (_useSinglePoolInUniV3(tokenIn, tokenOut) > 0 ? 0 : 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, @@ -209,130 +250,212 @@ 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); - - 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 + + // check pool existence first before quote against it + bool _univ2 = (router == UNIV2_ROUTER); + + (address _pool, address _token0, ) = pairForUniV2((_univ2? UNIV2_FACTORY : SUSHI_FACTORY), tokenIn, tokenOut, (_univ2? UNIV2_POOL_INITCODE : SUSHI_POOL_INITCODE)); + if (!_pool.isContract()){ + return 0; } - - return quote; + + bool _zeroForOne = (_token0 == tokenIn); + (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; } - + + /// @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 pure 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 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 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 _maxQuote; + uint24 _maxQuoteFee; - (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; } + { + // Heuristic: If we already know high TVL Pools, use those + uint24 _bestFee = _useSinglePoolInUniV3(tokenIn, tokenOut); + (address token0, address token1, bool token0Price) = _ifUniV3Token0Price(tokenIn, tokenOut); + + { + if (_bestFee > 0) { + (,uint256 _bestOutAmt) = _checkSimulationInUniV3(token0, token1, amountIn, _bestFee, token0Price); + return (_bestOutAmt, _bestFee); + } + } + + (uint256 _maxQAmt, uint24 _maxQFee) = _simLoopAllUniV3Pools(token0, token1, amountIn, token0Price); + _maxQuote = _maxQAmt; + _maxQuoteFee = _maxQFee; } - return quoteRate; - } + return (_maxQuote, _maxQuoteFee); + } - /// @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 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 + (, uint256 _outAmt) = _checkSimulationInUniV3(token0, token1, amountIn, _fee, token0Price); + if (_outAmt > _maxQuote){ + _maxQuote = _outAmt; + _maxQuoteFee = _fee; + } + unchecked { ++i; } + } } + + return (_maxQuote, _maxQuoteFee); } - /// @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 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, ) = _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 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 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 token0, address token1, uint256 amountIn, uint24 _fee, bool token0Price, address _pool) public view returns (bool, uint256){ + { + if (!_pool.isContract()) { + return (false, 0); + } + + bool _basicCheck = _checkPoolLiquidityAndBalances(IUniswapV3Pool(_pool).liquidity(), IERC20(token0Price? token0 : token1).balanceOf(_pool), amountIn); + if (!_basicCheck) { + return (false, 0); + } + + UniV3SortPoolQuery memory _sortQuery = UniV3SortPoolQuery(_pool, token0, token1, _fee, amountIn, token0Price); + return IUniswapV3Simulator(uniV3Simulator).checkInRangeLiquidity(_sortQuery); + } } - /// @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; + /// @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 token0, address token1, uint256 amountIn, uint24 _fee, bool token0Price) internal view returns (bool, uint256) { + bool _crossTick; + uint256 _outAmt; + + 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(token0, token1, amountIn, _fee, token0Price, _pool); + _crossTick = _outOfInRange; + _outAmt = _outputAmount; } - - // heuristic check1: ensure the pool tokenIn reserve makes sense in terms of [amountIn] - if (IERC20(token0Price? token0 : token1).balanceOf(pool) <= amountIn){ - return 0; + { + // unfortunately we need to do a full simulation to cross ticks + if (_crossTick){ + _outAmt = simulateUniV3Swap(token0, amountIn, token1, _fee, token0Price, _pool); + } } - - // 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; + 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(uint256 _liq, uint256 _reserveIn, uint256 amountIn) internal pure returns (bool) { + + { + // heuristic check0: ensure the pool initiated with valid liquidity in place + if (_liq == 0) { + return false; + } } - // 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; + { + // 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 + + // 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; + } } - /// @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 simulate Uniswap V3 swap using its tick-based math for given parameters + /// @dev check helper UniV3SwapSimulator for more + 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); + } + + /// @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, ) = sortUniV3Pools(tokenIn, amountIn, tokenOut); + return _maxInRangeQuote; } - /// @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 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) { + // Skip if there is a mainstrem direct swap or connector pools not exist + if (!checkUniV3PoolsExistence(tokenIn, connectorToken) || !checkUniV3PoolsExistence(connectorToken, tokenOut)){ + return 0; + } + + uint256 connectorAmount = getUniV3Price(tokenIn, amountIn, connectorToken); + if (connectorAmount > 0){ + return getUniV3Price(connectorToken, connectorAmount, tokenOut); + } else{ + return 0; + } } - /// @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 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 query with the address of the token0 & token1 & the fee tier @@ -341,100 +464,140 @@ 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) { + (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 && token1 == USDT) { + return 500; + } else if (token1 == USDC && token0 == DAI) { + return 100; + } else if (token0 == USDC && token1 == USDT) { + return 100; + } else if (token1 == USDC && token0 == WBTC) { + return 3000; + } else { + return 0; + } + } /// === 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) { + /// @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); if (poolId == BALANCERV2_NONEXIST_POOLID){ return 0; } + 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); + + { + (address[] memory tokens, uint256[] memory balances, ) = IBalancerV2Vault(BALANCERV2_VAULT).getPoolTokens(poolId); + + uint256 _inTokenIdx = _findTokenInBalancePool(tokenIn, tokens); + require(_inTokenIdx < tokens.length, "!inBAL"); + uint256 _outTokenIdx = _findTokenInBalancePool(tokenOut, tokens); + require(_outTokenIdx < tokens.length, "!outBAL"); + + if(balances[_inTokenIdx] <= amountIn) 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); + /// Balancer math for spot price of tokenIn -> tokenOut: weighted value(number * price) relation should be kept + 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()); + _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); + } + } + } - 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 _quote; } - /// @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){ + 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){ + return i; + } + unchecked{ ++i; } + } + return type(uint256).max; + } + + /// @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; } - bytes32 secondPoolId = getBalancerV2Pool(connectorToken, tokenOut); - if (secondPoolId == BALANCERV2_NONEXIST_POOLID){ + + uint256 _in2ConnectorAmt = getBalancerPriceAnalytically(tokenIn, amountIn, connectorToken); + if (_in2ConnectorAmt <= 0){ 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 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)){ + 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; - } 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; } @@ -462,8 +625,20 @@ 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); } + + /// @dev Take for example the _input "0x111122223333444455556666777788889999AAAABBBBCCCCDDDDEEEEFFFFCCCC" + /// @return the result of "0x111122223333444455556666777788889999aAaa" + 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 diff --git a/contracts/OnChainPricingMainnetLenient.sol b/contracts/OnChainPricingMainnetLenient.sol index 8b4a784..528025b 100644 --- a/contracts/OnChainPricingMainnetLenient.sol +++ b/contracts/OnChainPricingMainnetLenient.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0 pragma solidity 0.8.10; @@ -32,6 +32,12 @@ contract OnChainPricingMainnetLenient is OnChainPricingMainnet { uint256 public slippage = 200; // 2% Initially + constructor( + address _uniV3Simulator, + address _balancerV2Simulator + ) OnChainPricingMainnet(_uniV3Simulator, _balancerV2Simulator){ + // Silence is golden + } function setSlippage(uint256 newSlippage) external { require(msg.sender == TECH_OPS, "Only TechOps"); @@ -42,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 30476ec..bff461b 100644 --- a/contracts/OnChainSwapMainnet.sol +++ b/contracts/OnChainSwapMainnet.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0 pragma solidity 0.8.10; import {IERC20} from "@oz/token/ERC20/IERC20.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 @@ -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/UniV3SwapSimulator.sol b/contracts/UniV3SwapSimulator.sol new file mode 100644 index 0000000..2f38381 --- /dev/null +++ b/contracts/UniV3SwapSimulator.sol @@ -0,0 +1,184 @@ +// 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/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; +} + +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); + 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) 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 + uint160 _sqrtPriceLimitX96; + // Temporary state holding key data across swap steps + SwapStatus memory state; + + { + (uint160 _currentPX96, int24 _currentTick,,,,,) = IUniswapV3PoolSwapTick(_pool).slot0(); + _sqrtPriceLimitX96 = _getLimitPrice(_zeroForOne); + 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 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); + 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); + } + + /// @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, uint160 sqrtPriceNextX96) = _getNextInitializedTick(_nextTickQuery); + uint160 sqrtPriceStartX96 = state._sqrtPriceX96; + uint160 _targetPX96 = _getTargetPriceForSwapStep(_zeroForOne, sqrtPriceNextX96, _sqrtPriceLimitX96); + + /// 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 _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, _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; + } + + return (_swapAfterPrice, _tickNextPrice, _currentPriceX96); + } + + /// @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) { + 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); + + /// 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/VotiumBribesProcessor.sol b/contracts/VotiumBribesProcessor.sol index 6a8fca9..2d8b8cb 100644 --- a/contracts/VotiumBribesProcessor.sol +++ b/contracts/VotiumBribesProcessor.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0 pragma solidity 0.8.10; diff --git a/contracts/archive/BasicOnChainPricingMainnetLenient.sol b/contracts/archive/BasicOnChainPricingMainnetLenient.sol index 4440f32..f9f3a4c 100644 --- a/contracts/archive/BasicOnChainPricingMainnetLenient.sol +++ b/contracts/archive/BasicOnChainPricingMainnetLenient.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0 pragma solidity 0.8.10; @@ -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 @@ -75,7 +76,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 +110,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 +120,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 new file mode 100644 index 0000000..a9a8ebe --- /dev/null +++ b/contracts/archive/FullOnChainPricingMainnet.sol @@ -0,0 +1,459 @@ +// SPDX-License-Identifier: GPL-2.0 +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 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. +/// 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 + + // 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; + } 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 + 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 + function convertToBytes32(address _input) public pure returns (bytes32){ + return bytes32(uint256(uint160(_input)) << 96); + } +} \ No newline at end of file diff --git a/contracts/demo/CowSwapDemoSeller.sol b/contracts/demo/CowSwapDemoSeller.sol index c83d46a..5920c3c 100644 --- a/contracts/demo/CowSwapDemoSeller.sol +++ b/contracts/demo/CowSwapDemoSeller.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0 pragma solidity 0.8.10; diff --git a/contracts/demo/TestProcessor.sol b/contracts/demo/TestProcessor.sol index eaffb87..0f4cfdb 100644 --- a/contracts/demo/TestProcessor.sol +++ b/contracts/demo/TestProcessor.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0 pragma solidity 0.8.10; diff --git a/contracts/demo/UselessPricer.sol b/contracts/demo/UselessPricer.sol index ed60dee..dd08c45 100644 --- a/contracts/demo/UselessPricer.sol +++ b/contracts/demo/UselessPricer.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: GPL-2.0 pragma solidity 0.8.10; /// @title OnChainPricing diff --git a/contracts/libraries/balancer/BalancerFixedPoint.sol b/contracts/libraries/balancer/BalancerFixedPoint.sol new file mode 100644 index 0000000..108c75b --- /dev/null +++ b/contracts/libraries/balancer/BalancerFixedPoint.sol @@ -0,0 +1,113 @@ +// 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 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'); + + 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/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/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/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/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..9c13ca2 --- /dev/null +++ b/contracts/libraries/uniswap/SwapMath.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.7.6; +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 + /// @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) { + 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) + : 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); + } + } + + 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 >= 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/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/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/interfaces/balancer/IBalancerV2Simulator.sol b/interfaces/balancer/IBalancerV2Simulator.sol new file mode 100644 index 0000000..d8a950a --- /dev/null +++ b/interfaces/balancer/IBalancerV2Simulator.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +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 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/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..6b8573c --- /dev/null +++ b/interfaces/balancer/IBalancerV2WeightedPool.sol @@ -0,0 +1,8 @@ +// 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); + function getSwapFeePercentage() external view returns (uint256); +} 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 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 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..5f5c6d8 --- /dev/null +++ b/interfaces/uniswap/IV3Simulator.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +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; +} + +interface IUniswapV3Simulator { + 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/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 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..7c33946 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,18 @@ from time import time from brownie import * +from brownie import ( + accounts, + interface, + UniV3SwapSimulator, + BalancerSwapSimulator, + OnChainPricingMainnet, + CowSwapDemoSeller, + VotiumBribesProcessor, + AuraBribesProcessor, + OnChainPricingMainnetLenient, + FullOnChainPricingMainnet, + OnChainSwapMainnet +) import eth_abi from rich.console import Console import pytest @@ -14,58 +27,104 @@ CVX = "0x4e3fbd56cd56c3e72c1403e103b45db9da5b9d2b" DAI = "0x6b175474e89094c44da98b954eedeac495271d0f" WBTC = "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" +OHM="0x64aa3364F17a4D01c6f1751Fd97C2BD3D7e7f1D5" USDC_WHALE = "0x0a59649758aa4d66e25f08dd01271e891fe52199" BADGER_WHALE = "0xd0a7a8b98957b9cd3cfb9c0425abe44551158e9e" CVX_WHALE = "0xcf50b810e57ac33b91dcf525c6ddd9881b139332" DAI_WHALE = "0xe78388b4ce79068e89bf8aa7f218ef6b9ab0e9d0" AURA = "0xC0c293ce456fF0ED870ADd98a0828Dd4d2903DBF" +AURABAL = "0x616e8BfA43F920657B3497DBf40D6b1A02D4608d" BVE_CVX = "0xfd05D3C7fe2924020620A8bE4961bBaA747e6305" BVE_AURA = "0xBA485b556399123261a5F9c95d413B4f93107407" AURA_WHALE = "0x43B17088503F4CE1AED9fB302ED6BB51aD6694Fa" BALANCER_VAULT = "0xBA12222222228d8Ba445958a75a0704d566BF2C8" 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" WBTC_WHALE = "0xbf72da2bd84c5170618fbe5914b0eca9638d5eb5" ## Contracts ## + @pytest.fixture def swapexecutor(): - return OnChainSwapMainnet.deploy({"from": a[0]}) + 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(): - return OnChainPricingMainnet.deploy({"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": accounts[0]}) @pytest.fixture def lenient_contract(): ## NOTE: We have 5% slippage on this one - c = OnChainPricingMainnetLenient.deploy({"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(): 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) + +@pytest.fixture +def aurabal(): + return interface.ERC20(AURABAL) + +@pytest.fixture +def ohm(): + return interface.ERC20(OHM) + @pytest.fixture def wbtc(): return interface.ERC20(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(): diff --git a/tests/gas_benchmark/benchmark_pricer_gas.py b/tests/gas_benchmark/benchmark_pricer_gas.py new file mode 100644 index 0000000..1922774 --- /dev/null +++ b/tests/gas_benchmark/benchmark_pricer_gas.py @@ -0,0 +1,94 @@ +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, 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[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, 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[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, pricerwrapper): + pricer = pricerwrapper + token = aura # some swap (AURA-WETH) only in Balancer V2 + ## 1e18 + sell_count = 8000 + sell_amount = sell_count * oneE18 ## 1e18 + + tx = pricer.findOptimalSwap(token, weth.address, sell_amount) + 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, pricerwrapper): + pricer = pricerwrapper + token = aura # some swap (AURA-WETH-WBTC) only in Balancer V2 via WETH in between as connector + ## 1e18 + sell_count = 8000 + sell_amount = sell_count * oneE18 ## 1e18 + + tx = pricer.findOptimalSwap(token, wbtc.address, sell_amount) + 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, 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[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, 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[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, 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[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 new file mode 100644 index 0000000..6e3a300 --- /dev/null +++ b/tests/gas_benchmark/benchmark_token_coverage.py @@ -0,0 +1,60 @@ +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, pricerwrapper): + pricer = pricerwrapper + sell_token = token + ## 1e18 + sell_count = count + sell_amount = sell_count * oneE18 ## 1e18 + + 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 new file mode 100644 index 0000000..d458f5b --- /dev/null +++ b/tests/heuristic_equivalency/test_heuristic_equivalency.py @@ -0,0 +1,169 @@ +from rich.console import Console + +import pytest +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, 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[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[0] > 0 and tx[0] < tx2.gas_used + +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[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[0] > 0 and tx[0] < tx2.gas_used + +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 = 8000 + sell_amount = sell_count * oneE18 ## 1e18 + + tx = pricer.findOptimalSwap(token, weth.address, sell_amount) + 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[0] > 0 and tx[0] < tx2.gas_used + +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 = 8000 + sell_amount = sell_count * oneE18 ## 1e18 + + tx = pricer.findOptimalSwap(token, wbtc.address, sell_amount) + 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[0] > 0 and tx[0] < tx2.gas_used + +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[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[0] > 0 and tx[0] < tx2.gas_used + +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[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[0] > 0 and tx[0] < tx2.gas_used + +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[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[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[0] > 0 and tx[0] < 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 01d0828..11718f7 100644 --- a/tests/on_chain_pricer/test_balancer_pricer.py +++ b/tests/on_chain_pricer/test_balancer_pricer.py @@ -1,13 +1,29 @@ 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 +import pytest + +""" + 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 + 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() + quote = pricer.getBalancerQuoteWithinPoolAnalytcially(poolId, dai.address, sell_amount, usdc.address) + 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 @@ -15,16 +31,17 @@ 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 - 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 + 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 @@ -32,23 +49,114 @@ 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).return_value - assert quote >= p + p = sell_count * 15000 * 1000000 + quote = pricer.getBalancerPriceWithConnectorAnalytically(wbtc.address, sell_amount, usdc.address, weth.address) + 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' - 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 + +""" + 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 + +""" + 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 * 1000000000 ## 1e9 + + ## minimum quote for OHM in DAI(1e18) + p = sell_count * 10 * oneE18 + quote = pricer.getBalancerPriceAnalytically(ohm.address, sell_amount, dai.address) + 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 """ 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_aurabal_bpt_analytical(oneE18, aurabal, balethbpt, pricerwrapper): + pricer = pricerwrapper ## 1e18 - sell_amount = 100 * oneE18 + 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 - ## 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 + ## there is a proper pool in Balancer for AURABAL in BAL-ETH bpt + 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 + 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 WETH -> BADGER in Balancer V2 + quoteNoPool = pricer.getBalancerPriceAnalytically(weth.address, sell_amount, badger.address) + assert quoteNoPool == 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, wbtc, pricer): + ## bveaura + nonExistPool = pricer.BALANCERV2_NONEXIST_POOLID() + 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 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(), 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(), 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 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_bribe_tokens_supported.py b/tests/on_chain_pricer/test_bribe_tokens_supported.py index 8172fd1..c2d23af 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, @@ -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 @@ -56,7 +57,9 @@ 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 - + + quote = pricer.findOptimalSwap.call(token, WETH, AMOUNT) + assert quote[1][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..927d3a0 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 @@ -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.getUniV3Price(wbtc.address, sell_amount, usdc.address).return_value - assert quote >= p + p = 1 * 15000 * 1000000 ## swap on chain slippageTolerance = 0.95 wbtc.transfer(swapexecutor.address, sell_amount, {'from': wbtc_whale}) - minOutput = quote * 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.getUniV3PriceWithConnector(wbtc.address, sell_amount, usdc.address, weth.address).return_value - assert quote >= p ## swap on chain slippageTolerance = 0.95 wbtc.transfer(swapexecutor.address, sell_amount, {'from': wbtc_whale}) - minOutput = quote * 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 b1f28e1..f93ee4a 100644 --- a/tests/on_chain_pricer/test_univ3_pricer.py +++ b/tests/on_chain_pricer/test_univ3_pricer.py @@ -1,42 +1,78 @@ import brownie from brownie import * -import pytest +import pytest """ - getUniV3Price quote for token A swapped to token B directly: A - > B + test case for COW token to fix reported issue https://github.com/GalloDaSballo/fair-selling/issues/26 """ -def test_get_univ3_price(oneE18, weth, usdc, pricer): +def test_get_univ3_price_cow(oneE18, weth, usdc_whale, pricer): ## 1e18 - sell_amount = 1 * oneE18 + token = "0xdef1ca1fb7fbcdc777520aa7f396b4e015f497ab" + sell_count = 12209 + sell_amount = sell_count * oneE18 - ## 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 + ## 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 - 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 """ - getUniV3PriceWithConnector quote for token A swapped to token B with connector token C: A -> C -> B + getUniV3Price quote for token A swapped to token B directly: A - > B """ -def test_get_univ3_price_with_connector(oneE18, wbtc, usdc, weth, pricer): - ## 1e8 - sell_amount = 100 * 100000000 +def test_get_univ3_price_in_range(oneE18, weth, usdc, usdc_whale, pricer): + ## 1e18 + sell_count = 1 + sell_amount = sell_count * oneE18 + + ## minimum quote for ETH in USDC(1e6) ## Rip ETH price + p = sell_count * 900 * 1000000 + quoteInRange = pricer.checkUniV3InRangeLiquidity(usdc.address, weth.address, sell_amount, 500, False, "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640") + assert quoteInRange[1] >= p - quote = pricer.getUniV3Price(wbtc.address, sell_amount, usdc.address).return_value - quoteWithConnector = pricer.getUniV3PriceWithConnector(wbtc.address, sell_amount, usdc.address, weth.address).return_value - - ## min price - assert quoteWithConnector > quote + ## check against quoter + quoterP = interface.IV3Quoter(pricer.UNIV3_QUOTER()).quoteExactInputSingle.call(weth.address, usdc.address, 500, sell_amount, 0, {'from': usdc_whale.address}) + assert quoterP == quoteInRange[1] """ - getUniV3PriceWithConnector quote for stablecoin A swapped to stablecoin B with connector token C: A -> C -> B + getUniV3Price quote for token A swapped to token B directly: A - > B """ -def test_get_univ3_price_with_connector_stablecoin(oneE18, dai, usdc, weth, pricer): +def test_get_univ3_price_cross_tick(oneE18, weth, usdc, usdc_whale, pricer): ## 1e18 - sell_amount = 10000 * oneE18 + sell_count = 2000 + sell_amount = sell_count * oneE18 + + ## minimum quote for ETH in USDC(1e6) ## Rip ETH price + 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.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 - quote = pricer.getUniV3Price(dai.address, sell_amount, usdc.address).return_value - quoteWithConnector = pricer.getUniV3PriceWithConnector(dai.address, sell_amount, usdc.address, weth.address).return_value +""" + 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, dai, pricer): + ## 1e8 + sell_amount = 100 * 100000000 + + ## minimum quote for WBTC in USDC(1e6) + 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 - assert quoteWithConnector < quote \ No newline at end of file + assert quoteWithConnector >= p + + ## 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 new file mode 100644 index 0000000..5193dc6 --- /dev/null +++ b/tests/on_chain_pricer/test_univ3_pricer_simu.py @@ -0,0 +1,101 @@ +import brownie +from brownie import * +import pytest + +""" + 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 + +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 + + 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 + + ## 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 + + 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 + + ## 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 + +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, badger, aura, pricerwrapper): + pricer = pricerwrapper + ## 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[1][1] > 0 + assert quoteTx[1][0] == 0 + + ## not supported yet + isBadgerAuraSupported = pricer.isPairSupported(badger.address, aura.address, sell_amount * 100) + assert isBadgerAuraSupported == False + \ No newline at end of file