├── brownie_hooks.py ├── .gitattributes ├── .env.example ├── .gitignore ├── tests ├── coswap_seller │ ├── test_reverts.py │ └── test_cowswap_lifecycle.py ├── bribes_processor │ ├── test_ragequit.py │ ├── test_emit.py │ ├── test_sell_bribes_weth.py │ └── test_swap_weth_badger_cvx.py ├── aura_processor │ ├── test_ragequit_aura.py │ ├── test_real_life_revert.py │ ├── test_sell_bribes_weth_aura.py │ ├── test_swap_weth_badger_aura.py │ ├── test_real_life.py │ ├── test_emit_aura.py │ ├── run_fuzz_real_deploys.py │ ├── run_fuzz_integration_preview.py │ └── run_fuzz.py ├── on_chain_pricer │ ├── test_bribe_tokens_supported.py │ ├── test_univ3_pricer.py │ ├── test_univ3_pricer_simu.py │ ├── test_swap_exec_on_chain.py │ ├── run_fuzz.py │ └── test_balancer_pricer.py ├── gas_benchmark │ ├── benchmark_token_coverage.py │ └── benchmark_pricer_gas.py └── heuristic_equivalency │ └── test_heuristic_equivalency.py ├── requirements.txt ├── interfaces ├── uniswap │ ├── IV2Pool.sol │ ├── IV3Simulator.sol │ ├── IV3Pool.sol │ ├── IUniswapRouterV3.sol │ ├── IV3Quoter.sol │ └── IUniswapRouterV2.sol ├── balancer │ ├── IBalancerV2WeightedPool.sol │ ├── IBalancerV2StablePool.sol │ ├── IAsset.sol │ ├── IBalancerV2Simulator.sol │ ├── IBalancerV2Vault.sol │ └── WeightedPoolUserData.sol ├── badger │ ├── IHarvestForwarder.sol │ ├── ISettV4.sol │ └── IVault.sol ├── cowswap │ └── ICowSettlement.sol └── curve │ ├── ICurvePool.sol │ └── ICurveRouter.sol ├── helpers └── utils.py ├── contracts ├── libraries │ ├── uniswap │ │ ├── FixedPoint96.sol │ │ ├── UnsafeMath.sol │ │ ├── LiquidityMath.sol │ │ ├── SafeCast.sol │ │ ├── LowGasSafeMath.sol │ │ ├── BitMath.sol │ │ ├── TickBitmap.sol │ │ ├── FullMath.sol │ │ ├── SwapMath.sol │ │ └── TickMath.sol │ └── balancer │ │ ├── BalancerMath.sol │ │ ├── BalancerQuoter.sol │ │ ├── BalancerFixedPoint.sol │ │ └── BalancerStableMath.sol ├── demo │ ├── UselessPricer.sol │ ├── CowSwapDemoSeller.sol │ └── TestProcessor.sol ├── tests │ └── PricerWrapper.sol ├── OnChainPricingMainnetLenient.sol ├── archive │ └── BasicOnChainPricingMainnetLenient.sol ├── BalancerSwapSimulator.sol └── UniV3SwapSimulator.sol ├── brownie-config.yml ├── scripts ├── get_price.py ├── send_order_rinkeby.py └── send_order.py └── README.md /brownie_hooks.py: -------------------------------------------------------------------------------- 1 | from dotenv import load_dotenv 2 | 3 | load_dotenv() 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity 2 | *.vy linguist-language=Python 3 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | ETHERSCAN_TOKEN= 2 | WEB3_INFURA_PROJECT_ID= 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Brownie 2 | __pycache__ 3 | .history 4 | .hypothesis/ 5 | build/ 6 | reports/ 7 | .env 8 | venv/ 9 | 10 | # Node/npm 11 | node_modules/ 12 | -------------------------------------------------------------------------------- /tests/coswap_seller/test_reverts.py: -------------------------------------------------------------------------------- 1 | """ 2 | A bunch of test that MUST fail 3 | 4 | [] Swap from non manager 5 | [] Invalid order id 6 | [] Order Fee higher than 10% 7 | """ -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | black==21.9b0 2 | eth-brownie>=1.11.0,<2.0.0 3 | dotmap==1.3.24 4 | python-dotenv==0.16.0 5 | tabulate==0.8.9 6 | rich==10.7.0 7 | click==8.0.1 8 | platformdirs==2.3.0 9 | regex==2021.8.28 -------------------------------------------------------------------------------- /interfaces/uniswap/IV2Pool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity 0.8.10; 3 | pragma abicoder v2; 4 | 5 | interface IUniswapV2Pool { 6 | function getReserves() external view returns (uint256 reserve0, uint256 reserve1, uint32 blockTimestampLast); 7 | } -------------------------------------------------------------------------------- /helpers/utils.py: -------------------------------------------------------------------------------- 1 | 2 | # Assert approximate integer 3 | def approx(actual, expected, percentage_threshold): 4 | print(actual, expected, percentage_threshold) 5 | diff = int(abs(actual - expected)) 6 | # 0 diff should automtically be a match 7 | if diff == 0: 8 | return True 9 | return diff < (actual * percentage_threshold // 100) -------------------------------------------------------------------------------- /interfaces/balancer/IBalancerV2WeightedPool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.7.5; 3 | pragma abicoder v2; 4 | 5 | interface IBalancerV2WeightedPool { 6 | function getNormalizedWeights() external view returns (uint256[] memory); 7 | function getSwapFeePercentage() external view returns (uint256); 8 | } 9 | -------------------------------------------------------------------------------- /contracts/libraries/uniswap/FixedPoint96.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.7.6; 3 | pragma abicoder v2; 4 | 5 | // https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/FixedPoint96.sol 6 | library FixedPoint96 { 7 | uint8 internal constant RESOLUTION = 96; 8 | uint256 internal constant Q96 = 0x1000000000000000000000000; 9 | } -------------------------------------------------------------------------------- /interfaces/badger/IHarvestForwarder.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity >=0.6.11; 4 | pragma experimental ABIEncoderV2; 5 | 6 | interface IHarvestForwarder { 7 | function distribute( 8 | address token, 9 | uint256 amount, 10 | address beneficiary 11 | ) external; 12 | function badger_tree() external view returns(address); 13 | } 14 | -------------------------------------------------------------------------------- /interfaces/balancer/IBalancerV2StablePool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.7.5; 3 | pragma abicoder v2; 4 | 5 | interface IBalancerV2StablePool { 6 | function getAmplificationParameter() external view returns (uint256 value, bool isUpdating, uint256 precision); 7 | function getSwapFeePercentage() external view returns (uint256); 8 | } 9 | -------------------------------------------------------------------------------- /interfaces/cowswap/ICowSettlement.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.5.0; 3 | pragma experimental ABIEncoderV2; 4 | 5 | 6 | interface ICowSettlement { 7 | function setPreSignature(bytes calldata orderUid, bool signed) external; 8 | function preSignature(bytes calldata orderUid) external view returns (uint256); 9 | function domainSeparator() external view returns (bytes32); 10 | } -------------------------------------------------------------------------------- /interfaces/balancer/IAsset.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.10; 3 | 4 | /** 5 | * @dev This is an empty interface used to represent either ERC20-conforming token contracts or ETH (using the zero 6 | * address sentinel value). We're just relying on the fact that `interface` can be used to declare new address-like 7 | * types. 8 | * 9 | * This concept is unrelated to a Pool's Asset Managers. 10 | */ 11 | interface IAsset { 12 | // solhint-disable-previous-line no-empty-blocks 13 | } 14 | -------------------------------------------------------------------------------- /interfaces/curve/ICurvePool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.5.0; 3 | pragma experimental ABIEncoderV2; 4 | 5 | 6 | interface ICurvePool { 7 | function coins(uint256 n) external view returns (address); 8 | function get_dy(int128 i, int128 j, uint256 dx) external view returns (uint256); 9 | function exchange(int128 i, int128 j, uint256 _dx, uint256 _min_dy) external returns (uint256); 10 | function get_virtual_price() external view returns (uint256); 11 | function get_balances() external view returns (uint256[] memory); 12 | function fee() external view returns (uint256); 13 | } -------------------------------------------------------------------------------- /contracts/demo/UselessPricer.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0 2 | pragma solidity 0.8.10; 3 | 4 | /// @title OnChainPricing 5 | /// @author Alex the Entreprenerd @ BadgerDAO 6 | /// @dev Always returns 0 making all cowswap trades go through (for testnet bruh) 7 | contract UselessPricer { 8 | 9 | struct Quote { 10 | string name; 11 | uint256 amountOut; 12 | } 13 | 14 | 15 | /// @dev View function for testing the routing of the strategy 16 | function findOptimalSwap(address tokenIn, address tokenOut, uint256 amountIn) external view returns (Quote memory) { 17 | return Quote("fake", 0); 18 | } 19 | } -------------------------------------------------------------------------------- /contracts/libraries/uniswap/UnsafeMath.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.7.6; 3 | pragma abicoder v2; 4 | 5 | // https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/UnsafeMath.sol 6 | library UnsafeMath { 7 | /// @notice Returns ceil(x / y) 8 | /// @dev division by 0 has unspecified behavior, and must be checked externally 9 | /// @param x The dividend 10 | /// @param y The divisor 11 | /// @return z The quotient, ceil(x / y) 12 | function divRoundingUp(uint256 x, uint256 y) internal pure returns (uint256 z) { 13 | assembly { 14 | z := add(div(x, y), gt(mod(x, y), 0)) 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /interfaces/badger/ISettV4.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity >=0.6.11; 4 | pragma experimental ABIEncoderV2; 5 | 6 | interface ISettV4 { 7 | function deposit(uint256 _amount) external; 8 | 9 | function depositFor(address _recipient, uint256 _amount) external; 10 | 11 | function withdraw(uint256 _amount) external; 12 | 13 | function balance() external view returns (uint256); 14 | 15 | function getPricePerFullShare() external view returns (uint256); 16 | 17 | function balanceOf(address) external view returns (uint256); 18 | function totalSupply() external view returns (uint256); 19 | function approveContractAccess(address) external; 20 | function governance() external view returns (address); 21 | 22 | } 23 | -------------------------------------------------------------------------------- /contracts/libraries/uniswap/LiquidityMath.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.7.6; 3 | pragma abicoder v2; 4 | 5 | // https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/LiquidityMath.sol 6 | library LiquidityMath { 7 | /// @notice Add a signed liquidity delta to liquidity and revert if it overflows or underflows 8 | /// @param x The liquidity before change 9 | /// @param y The delta by which liquidity should be changed 10 | /// @return z The liquidity delta 11 | function addDelta(uint128 x, int128 y) internal pure returns (uint128 z) { 12 | if (y < 0) { 13 | require((z = x - uint128(-y)) < x, 'LS'); 14 | } else { 15 | require((z = x + uint128(y)) >= x, 'LA'); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /brownie-config.yml: -------------------------------------------------------------------------------- 1 | # use Ganache's forked mainnet mode as the default network 2 | # NOTE: You don't *have* to do this, but it is often helpful for testing 3 | networks: 4 | default: mainnet-fork 5 | 6 | # automatically fetch contract sources from Etherscan 7 | autofetch_sources: True 8 | 9 | # require OpenZepplin Contracts 10 | dependencies: 11 | - OpenZeppelin/openzeppelin-contracts@4.5.0 12 | 13 | # path remapping to support imports from GitHub/NPM 14 | compiler: 15 | solc: 16 | # version: 0.8.10 17 | remappings: 18 | - "@oz=OpenZeppelin/openzeppelin-contracts@4.5.0/contracts/" 19 | 20 | reports: 21 | exclude_contracts: 22 | - SafeERC20 23 | - IERC20 24 | - ReentrancyGuard 25 | - Address 26 | 27 | 28 | hypothesis: 29 | max_examples: 1000 -------------------------------------------------------------------------------- /interfaces/uniswap/IV3Simulator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity 0.8.10; 3 | pragma abicoder v2; 4 | 5 | // Uniswap V3 simulation query 6 | struct TickNextWithWordQuery{ 7 | address pool; 8 | int24 tick; 9 | int24 tickSpacing; 10 | bool lte; 11 | } 12 | 13 | struct UniV3SortPoolQuery{ 14 | address _pool; 15 | address _token0; 16 | address _token1; 17 | uint24 _fee; 18 | uint256 amountIn; 19 | bool zeroForOne; 20 | } 21 | 22 | interface IUniswapV3Simulator { 23 | function simulateUniV3Swap(address _pool, address _token0, address _token1, bool _zeroForOne, uint24 _fee, uint256 _amountIn) external view returns (uint256); 24 | function checkInRangeLiquidity(UniV3SortPoolQuery memory _sortQuery) external view returns (bool, uint256); 25 | } -------------------------------------------------------------------------------- /interfaces/curve/ICurveRouter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.5.0; 3 | pragma experimental ABIEncoderV2; 4 | 5 | 6 | interface ICurveRouter { 7 | function get_best_rate( 8 | address from, address to, uint256 _amount) external view returns (address, uint256); 9 | 10 | function exchange_with_best_rate( 11 | address _from, 12 | address _to, 13 | uint256 _amount, 14 | uint256 _expected 15 | ) external returns (uint256); 16 | 17 | function exchange_with_best_rate( 18 | address _from, 19 | address _to, 20 | uint256 _amount, 21 | uint256 _expected, 22 | address _receiver 23 | ) external returns (uint256); 24 | 25 | function exchange( 26 | address _pool, 27 | address _from, 28 | address _to, 29 | uint256 _amount, 30 | uint256 _expected, 31 | address _receiver 32 | ) external returns (uint256); 33 | } -------------------------------------------------------------------------------- /interfaces/balancer/IBalancerV2Simulator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity 0.8.10; 3 | pragma abicoder v2; 4 | 5 | struct ExactInQueryParam{ 6 | address tokenIn; 7 | address tokenOut; 8 | uint256 balanceIn; 9 | uint256 weightIn; 10 | uint256 balanceOut; 11 | uint256 weightOut; 12 | uint256 amountIn; 13 | uint256 swapFeePercentage; 14 | } 15 | 16 | struct ExactInStableQueryParam{ 17 | address[] tokens; 18 | uint256[] balances; 19 | uint256 currentAmp; 20 | uint256 tokenIndexIn; 21 | uint256 tokenIndexOut; 22 | uint256 amountIn; 23 | uint256 swapFeePercentage; 24 | } 25 | 26 | interface IBalancerV2Simulator { 27 | function calcOutGivenIn(ExactInQueryParam memory _query) external view returns (uint256); 28 | function calcOutGivenInForStable(ExactInStableQueryParam memory _query) external view returns (uint256); 29 | } -------------------------------------------------------------------------------- /interfaces/uniswap/IV3Pool.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity 0.8.10; 3 | pragma abicoder v2; 4 | 5 | interface IUniswapV3Pool { 6 | function slot0() external view returns (uint160 sqrtPriceX96, int24, uint16, uint16, uint16, uint8, bool); 7 | function liquidity() external view returns (uint128); 8 | function tickBitmap(int16 wordPosition) external view returns (uint256); 9 | function tickSpacing() external view returns (int24); 10 | function fee() external view returns (uint24); 11 | function token0() external view returns (address); 12 | function token1() external view returns (address); 13 | function ticks(int24 tick) external view returns (uint128 liquidityGross, int128 liquidityNet, uint256 feeGrowthOutside0X128, uint256 feeGrowthOutside1X128, int56 tickCumulativeOutside, uint160 secondsPerLiquidityOutsideX128, uint32 secondsOutside, bool initialized); 14 | } -------------------------------------------------------------------------------- /tests/coswap_seller/test_cowswap_lifecycle.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | from brownie import * 3 | from scripts.send_order import get_cowswap_order 4 | 5 | """ 6 | Verify order 7 | Place order 8 | """ 9 | 10 | def test_verify_and_add_order(seller, usdc, weth, manager, pricer): 11 | sell_amount = 1000000000000000000 12 | 13 | ## TODO Use the Hash Map here to verify it 14 | order_details = get_cowswap_order(seller, weth, usdc, sell_amount) 15 | 16 | data = order_details.order_data 17 | uid = order_details.order_uid 18 | 19 | ## Verify that the ID Matches 20 | assert uid == seller.getOrderID(data) 21 | 22 | ## Verify that the quote is better than what we could do 23 | assert seller.checkCowswapOrder(data, uid) 24 | 25 | ## Place the order 26 | ## Get settlement here and check 27 | seller.initiateCowswapOrder(data, uid, {"from": manager}) 28 | 29 | settlement = interface.ICowSettlement(seller.SETTLEMENT()) 30 | 31 | assert settlement.preSignature(uid) > 0 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /contracts/demo/CowSwapDemoSeller.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0 2 | pragma solidity 0.8.10; 3 | 4 | 5 | import {CowSwapSeller} from "../CowSwapSeller.sol"; 6 | 7 | import {IERC20} from "@oz/token/ERC20/IERC20.sol"; 8 | import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol"; 9 | 10 | contract CowSwapDemoSeller is CowSwapSeller { 11 | using SafeERC20 for IERC20; 12 | 13 | constructor(address _pricer) CowSwapSeller(_pricer) {} 14 | 15 | function initiateCowswapOrder(Data calldata orderData, bytes memory orderUid) external { 16 | _doCowswapOrder(orderData, orderUid); 17 | } 18 | 19 | 20 | function cancelCowswapOrder(bytes memory orderUid) external { 21 | _cancelCowswapOrder(orderUid); 22 | } 23 | 24 | function sendTokenBack(IERC20 token) external nonReentrant { 25 | require(msg.sender == manager); 26 | 27 | token.safeApprove(RELAYER, 0); // Remove approval in case we had pending order 28 | token.safeTransfer(manager, token.balanceOf(address(this))); 29 | } 30 | } -------------------------------------------------------------------------------- /contracts/libraries/balancer/BalancerMath.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.7.6; 3 | pragma abicoder v2; 4 | 5 | // https://github.com/balancer-labs/balancer-v2-monorepo/blob/master/pkg/solidity-utils/contracts/math/Math.sol 6 | library BalancerMath { 7 | 8 | function div(uint256 a, uint256 b, bool roundUp) internal pure returns (uint256) { 9 | return roundUp ? divUp(a, b) : divDown(a, b); 10 | } 11 | 12 | function mul(uint256 a, uint256 b) internal pure returns (uint256) { 13 | uint256 c = a * b; 14 | require(a == 0 || c / a == b, '!OVEF'); 15 | return c; 16 | } 17 | 18 | function divDown(uint256 a, uint256 b) internal pure returns (uint256) { 19 | require(b != 0, '!b0'); 20 | return a / b; 21 | } 22 | 23 | function divUp(uint256 a, uint256 b) internal pure returns (uint256) { 24 | require(b != 0, '!b0'); 25 | 26 | if (a == 0) { 27 | return 0; 28 | } else { 29 | return 1 + (a - 1) / b; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /contracts/libraries/uniswap/SafeCast.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.7.6; 3 | pragma abicoder v2; 4 | 5 | // https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/SafeCast.sol 6 | library SafeCast { 7 | /// @notice Cast a uint256 to a uint160, revert on overflow 8 | /// @param y The uint256 to be downcasted 9 | /// @return z The downcasted integer, now type uint160 10 | function toUint160(uint256 y) internal pure returns (uint160 z) { 11 | require((z = uint160(y)) == y); 12 | } 13 | 14 | /// @notice Cast a int256 to a int128, revert on overflow or underflow 15 | /// @param y The int256 to be downcasted 16 | /// @return z The downcasted integer, now type int128 17 | function toInt128(int256 y) internal pure returns (int128 z) { 18 | require((z = int128(y)) == y); 19 | } 20 | 21 | /// @notice Cast a uint256 to a int256, revert on overflow 22 | /// @param y The uint256 to be casted 23 | /// @return z The casted integer, now type int256 24 | function toInt256(uint256 y) internal pure returns (int256 z) { 25 | require(y < 2**255); 26 | z = int256(y); 27 | } 28 | } -------------------------------------------------------------------------------- /tests/bribes_processor/test_ragequit.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | from brownie import * 3 | 4 | """ 5 | Unit tests for all functions 6 | 7 | Ragequit 8 | Anytime from manager 9 | After 28 days from anyone 10 | """ 11 | 12 | 13 | ### Ragequit 14 | 15 | def test_ragequit_permission(setup_processor, manager, usdc): 16 | balance_before = usdc.balanceOf(setup_processor.BADGER_TREE()) 17 | 18 | ## The manager can RQ at any time 19 | setup_processor.ragequit(usdc, False, {"from": manager}) 20 | 21 | assert usdc.balanceOf(setup_processor.BADGER_TREE()) > balance_before 22 | 23 | 24 | def test_ragequit_anyone_after_time(setup_processor, manager, usdc): 25 | rando = accounts[2] 26 | 27 | ## Reverts if rando calls immediately 28 | with brownie.reverts(): 29 | setup_processor.ragequit(usdc, False, {"from": rando}) 30 | 31 | chain.sleep(setup_processor.MAX_MANAGER_IDLE_TIME() + 1) 32 | chain.mine() 33 | 34 | balance_before = usdc.balanceOf(setup_processor.BADGER_TREE()) 35 | 36 | ## Rando can call after time has passed 37 | setup_processor.ragequit(usdc, False, {"from": rando}) 38 | 39 | assert usdc.balanceOf(setup_processor.BADGER_TREE()) > balance_before -------------------------------------------------------------------------------- /interfaces/uniswap/IUniswapRouterV3.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.7.5; 3 | pragma abicoder v2; 4 | 5 | //A path is a sequence of token addresses and poolFees that define the pools used in the swaps. 6 | //The format for pool encoding(abi.abiEncodedPath) is (tokenIn, fee, connectorToken, fee, tokenOut) where connectorToken parameter is the shared token across the pools. 7 | //For example, if we are swapping DAI to USDC and then USDC to WETH the path encoding is (DAI, 0.3%, USDC, 0.3%, WETH). 8 | //and the path should be encoded like `abi.encodePacked(DAI, 3000, USDC, 3000, WETH)` 9 | //Note that fee is in hundredths of basis points (e.g. the fee for a pool at the 0.3% tier is 3000; the fee for a pool at the 0.01% tier is 100). 10 | struct ExactInputParams { 11 | bytes path; 12 | address recipient; 13 | uint256 amountIn; 14 | uint256 amountOutMinimum; 15 | } 16 | 17 | // https://github.com/Uniswap/swap-router-contracts/blob/v1.1.0/contracts/interfaces/IV3SwapRouter.sol 18 | // https://docs.uniswap.org/protocol/reference/deployments 19 | interface IUniswapRouterV3 { 20 | function exactInput(ExactInputParams calldata params) external returns (uint256 amountOut); 21 | } 22 | -------------------------------------------------------------------------------- /contracts/demo/TestProcessor.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0 2 | pragma solidity 0.8.10; 3 | 4 | 5 | import {ICurvePool} from "../../interfaces/curve/ICurvePool.sol"; 6 | import {ISettV4} from "../../interfaces/badger/ISettV4.sol"; 7 | import {CowSwapSeller} from "../CowSwapSeller.sol"; 8 | import {IERC20} from "@oz/token/ERC20/IERC20.sol"; 9 | import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol"; 10 | 11 | 12 | /// @title TestProcessor 13 | /// @author Alex the Entreprenerd @ BadgerDAO 14 | /// @dev Test processor for rinkeby 15 | contract TestProcessor is CowSwapSeller { 16 | using SafeERC20 for IERC20; 17 | 18 | constructor(address _pricer) CowSwapSeller(_pricer) {} 19 | 20 | 21 | /// @dev Recover tokens 22 | function sweep(address token) external { 23 | require(msg.sender == manager); 24 | IERC20(token).safeTransfer(manager, IERC20(token).balanceOf(address(this))); 25 | } 26 | 27 | 28 | function doCowswapOrder(Data calldata orderData, bytes memory orderUid) external { 29 | _doCowswapOrder(orderData, orderUid); 30 | } 31 | 32 | function cancelCowswapOrder(bytes memory orderUid) external { 33 | _cancelCowswapOrder(orderUid); 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /tests/aura_processor/test_ragequit_aura.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | from brownie import * 3 | 4 | """ 5 | Unit tests for all functions 6 | 7 | Ragequit 8 | Anytime from manager 9 | After 28 days from anyone 10 | """ 11 | 12 | 13 | ### Ragequit 14 | 15 | def test_ragequit_permission(setup_aura_processor, manager, usdc): 16 | balance_before = usdc.balanceOf(setup_aura_processor.BADGER_TREE()) 17 | 18 | ## The manager can RQ at any time 19 | setup_aura_processor.ragequit(usdc, False, {"from": manager}) 20 | 21 | assert usdc.balanceOf(setup_aura_processor.BADGER_TREE()) > balance_before 22 | 23 | 24 | def test_ragequit_anyone_after_time(setup_aura_processor, manager, usdc): 25 | rando = accounts[2] 26 | 27 | ## Reverts if rando calls immediately 28 | with brownie.reverts(): 29 | setup_aura_processor.ragequit(usdc, False, {"from": rando}) 30 | 31 | chain.sleep(setup_aura_processor.MAX_MANAGER_IDLE_TIME() + 1) 32 | chain.mine() 33 | 34 | balance_before = usdc.balanceOf(setup_aura_processor.BADGER_TREE()) 35 | 36 | ## Rando can call after time has passed 37 | setup_aura_processor.ragequit(usdc, False, {"from": rando}) 38 | 39 | assert usdc.balanceOf(setup_aura_processor.BADGER_TREE()) > balance_before -------------------------------------------------------------------------------- /interfaces/badger/IVault.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | 3 | pragma solidity 0.8.10; 4 | 5 | interface IVault { 6 | function token() external view returns (address); 7 | 8 | function reportHarvest(uint256 _harvestedAmount) external; 9 | 10 | function reportAdditionalToken(address _token) external; 11 | 12 | // Fees 13 | function performanceFeeGovernance() external view returns (uint256); 14 | 15 | function performanceFeeStrategist() external view returns (uint256); 16 | 17 | function withdrawalFee() external view returns (uint256); 18 | 19 | function managementFee() external view returns (uint256); 20 | 21 | // Actors 22 | function governance() external view returns (address); 23 | 24 | function keeper() external view returns (address); 25 | 26 | function guardian() external view returns (address); 27 | 28 | function strategist() external view returns (address); 29 | 30 | function treasury() external view returns (address); 31 | 32 | // External 33 | function deposit(uint256 _amount) external; 34 | 35 | function depositFor(address _recipient, uint256 _amount) external; 36 | 37 | function totalSupply() external returns (uint256); 38 | 39 | function balanceOf(address) external view returns (uint256); 40 | 41 | function balance() external view returns (uint256); 42 | 43 | function getPricePerFullShare() external view returns (uint256); 44 | } -------------------------------------------------------------------------------- /contracts/tests/PricerWrapper.sol: -------------------------------------------------------------------------------- 1 | pragma solidity 0.8.10; 2 | pragma experimental ABIEncoderV2; 3 | 4 | enum SwapType { 5 | CURVE, //0 6 | UNIV2, //1 7 | SUSHI, //2 8 | UNIV3, //3 9 | UNIV3WITHWETH, //4 10 | BALANCER, //5 11 | BALANCERWITHWETH //6 12 | } 13 | 14 | // Onchain Pricing Interface 15 | struct Quote { 16 | SwapType name; 17 | uint256 amountOut; 18 | bytes32[] pools; // specific pools involved in the optimal swap path 19 | uint256[] poolFees; // specific pool fees involved in the optimal swap path, typically in Uniswap V3 20 | } 21 | interface OnChainPricing { 22 | function isPairSupported(address tokenIn, address tokenOut, uint256 amountIn) external view returns (bool); 23 | function findOptimalSwap(address tokenIn, address tokenOut, uint256 amountIn) external view returns (Quote memory); 24 | } 25 | // END OnchainPricing 26 | 27 | contract PricerWrapper { 28 | address public pricer; 29 | constructor(address _pricer) { 30 | pricer = _pricer; 31 | } 32 | 33 | function isPairSupported(address tokenIn, address tokenOut, uint256 amountIn) external view returns (bool) { 34 | return OnChainPricing(pricer).isPairSupported(tokenIn, tokenOut, amountIn); 35 | } 36 | 37 | function findOptimalSwap(address tokenIn, address tokenOut, uint256 amountIn) external view returns (uint256, Quote memory) { 38 | uint256 _gasBefore = gasleft(); 39 | Quote memory q = OnChainPricing(pricer).findOptimalSwap(tokenIn, tokenOut, amountIn); 40 | return (_gasBefore - gasleft(), q); 41 | } 42 | } -------------------------------------------------------------------------------- /interfaces/balancer/IBalancerV2Vault.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.7.5; 3 | pragma abicoder v2; 4 | 5 | enum SwapKind { GIVEN_IN, GIVEN_OUT } 6 | 7 | struct BatchSwapStep { 8 | bytes32 poolId; 9 | uint256 assetInIndex; 10 | uint256 assetOutIndex; 11 | uint256 amount; 12 | bytes userData; 13 | } 14 | 15 | struct SingleSwap { 16 | bytes32 poolId; 17 | SwapKind kind; 18 | address assetIn; 19 | address assetOut; 20 | uint256 amount; 21 | bytes userData; 22 | } 23 | 24 | struct FundManagement { 25 | address sender; 26 | bool fromInternalBalance; 27 | address recipient; 28 | bool toInternalBalance; 29 | } 30 | 31 | enum PoolSpecialization { GENERAL, MINIMAL_SWAP_INFO, TWO_TOKEN } 32 | 33 | interface IBalancerV2Vault { 34 | function batchSwap(SwapKind kind, BatchSwapStep[] calldata swaps, address[] calldata assets, FundManagement calldata funds, int256[] calldata limits, uint256 deadline) external returns (int256[] memory assetDeltas); 35 | function queryBatchSwap(SwapKind kind, BatchSwapStep[] calldata swaps, address[] calldata assets, FundManagement calldata funds) external returns (int256[] memory assetDeltas); 36 | function swap(SingleSwap calldata singleSwap, FundManagement calldata funds, uint256 limit, uint256 deadline) external returns (uint256 amountCalculatedInOut); 37 | function getPool(bytes32 poolId) external view returns (address, PoolSpecialization); 38 | function getPoolTokens(bytes32 poolId) external view returns (address[] memory tokens, uint256[] memory balances, uint256 lastChangeBlock); 39 | } 40 | -------------------------------------------------------------------------------- /contracts/libraries/uniswap/LowGasSafeMath.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.7.6; 3 | pragma abicoder v2; 4 | 5 | // https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/LowGasSafeMath.sol 6 | library LowGasSafeMath { 7 | /// @notice Returns x + y, reverts if sum overflows uint256 8 | /// @param x The augend 9 | /// @param y The addend 10 | /// @return z The sum of x and y 11 | function add(uint256 x, uint256 y) internal pure returns (uint256 z) { 12 | require((z = x + y) >= x); 13 | } 14 | 15 | /// @notice Returns x - y, reverts if underflows 16 | /// @param x The minuend 17 | /// @param y The subtrahend 18 | /// @return z The difference of x and y 19 | function sub(uint256 x, uint256 y) internal pure returns (uint256 z) { 20 | require((z = x - y) <= x); 21 | } 22 | 23 | /// @notice Returns x * y, reverts if overflows 24 | /// @param x The multiplicand 25 | /// @param y The multiplier 26 | /// @return z The product of x and y 27 | function mul(uint256 x, uint256 y) internal pure returns (uint256 z) { 28 | require(x == 0 || (z = x * y) / x == y); 29 | } 30 | 31 | /// @notice Returns x + y, reverts if overflows or underflows 32 | /// @param x The augend 33 | /// @param y The addend 34 | /// @return z The sum of x and y 35 | function add(int256 x, int256 y) internal pure returns (int256 z) { 36 | require((z = x + y) >= x == (y >= 0)); 37 | } 38 | 39 | /// @notice Returns x - y, reverts if overflows or underflows 40 | /// @param x The minuend 41 | /// @param y The subtrahend 42 | /// @return z The difference of x and y 43 | function sub(int256 x, int256 y) internal pure returns (int256 z) { 44 | require((z = x - y) <= x == (y >= 0)); 45 | } 46 | } -------------------------------------------------------------------------------- /tests/bribes_processor/test_emit.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | from brownie import * 3 | 4 | """ 5 | swapCVXTobveCVXAndEmit 6 | Works for both LP and Buy 7 | Emits event 8 | 9 | emitBadger 10 | Works 11 | Emits event 12 | """ 13 | 14 | def test_swap_cvx_and_emit_with_purchase(setup_processor, manager, bve_cvx, cvx, make_cvx_pool_profitable): 15 | bve_balance_before = bve_cvx.balanceOf(setup_processor.BADGER_TREE()) 16 | assert cvx.balanceOf(setup_processor) > 0 17 | 18 | tx = setup_processor.swapCVXTobveCVXAndEmit({"from": manager}) 19 | 20 | assert bve_cvx.balanceOf(setup_processor.BADGER_TREE()) > bve_balance_before 21 | assert cvx.balanceOf(setup_processor.BADGER_TREE()) == 0 ## All CVX has been emitted 22 | 23 | 24 | ## Reverts if called a second time 25 | with brownie.reverts(): 26 | setup_processor.swapCVXTobveCVXAndEmit({"from": manager}) 27 | 28 | def test_swap_cvx_and_emit_with_deposit(setup_processor, manager, bve_cvx, cvx, make_cvx_pool_unprofitable): 29 | bve_balance_before = bve_cvx.balanceOf(setup_processor.BADGER_TREE()) 30 | assert cvx.balanceOf(setup_processor) > 0 31 | 32 | tx = setup_processor.swapCVXTobveCVXAndEmit({"from": manager}) 33 | 34 | assert bve_cvx.balanceOf(setup_processor.BADGER_TREE()) > bve_balance_before 35 | assert cvx.balanceOf(setup_processor.BADGER_TREE()) == 0 ## All CVX has been emitted 36 | 37 | 38 | ## Reverts if called a second time 39 | with brownie.reverts(): 40 | setup_processor.swapCVXTobveCVXAndEmit({"from": manager}) 41 | 42 | def test_emit_badger(setup_processor, manager, badger,): 43 | badger_balance_before = badger.balanceOf(setup_processor.BADGER_TREE()) 44 | assert badger.balanceOf(setup_processor) > 0 45 | 46 | setup_processor.emitBadger({"from": manager}) 47 | 48 | assert badger.balanceOf(setup_processor.BADGER_TREE()) > badger_balance_before 49 | assert badger.balanceOf(setup_processor) == 0 ## All badger emitted 50 | 51 | ## Reverts if called a second time 52 | with brownie.reverts(): 53 | setup_processor.emitBadger({"from": manager}) -------------------------------------------------------------------------------- /scripts/get_price.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from brownie import * 3 | 4 | import json 5 | 6 | """ 7 | Get quote for token by given id(/coin/list) from coingecko api https://www.coingecko.com/en/api/documentation 8 | """ 9 | def get_coingecko_price(coinID): 10 | quote_denomination = 'usd' 11 | 12 | quote = "https://api.coingecko.com/api/v3/simple/price" 13 | get_params = { 14 | "ids": coinID, 15 | "vs_currencies": quote_denomination 16 | } 17 | r = requests.get(quote, params=get_params) 18 | assert r.ok and r.status_code == 200 19 | 20 | price = float(r.json()[coinID][quote_denomination]) 21 | assert price > 0 22 | 23 | return price 24 | 25 | """ 26 | Get quote for token by given id(from metadata) from coinmarketcap api https://coinmarketcap.com/api/documentation 27 | """ 28 | def get_coinmarketcap_price(coinID, apiKey): 29 | quote_denomination = 'USD' 30 | 31 | quote = "https://pro-api.coinmarketcap.com/v2/cryptocurrency/quotes/latest" 32 | get_params = { 33 | "id": coinID, 34 | "convert": quote_denomination 35 | } 36 | get_headers = { 37 | 'Accepts': 'application/json', 38 | 'X-CMC_PRO_API_KEY': apiKey, 39 | } 40 | 41 | r = requests.get(quote, params=get_params, headers=get_headers) 42 | assert r.ok and r.status_code == 200 43 | 44 | price = float(r.json()['data'][coinID]['quote'][quote_denomination]['price']) 45 | assert price > 0 46 | 47 | return price 48 | 49 | """ 50 | Get quote for token by given slug(/coin/list) from coinmarketcap api https://coinmarketcap.com/api/documentation 51 | """ 52 | def get_coinmarketcap_metadata(coinSlug, apiKey): 53 | quote = "https://pro-api.coinmarketcap.com/v2/cryptocurrency/info" 54 | get_params = { 55 | "slug": coinSlug 56 | } 57 | get_headers = { 58 | 'Accepts': 'application/json', 59 | 'X-CMC_PRO_API_KEY': apiKey, 60 | } 61 | 62 | r = requests.get(quote, params=get_params, headers=get_headers) 63 | print(json.dumps(json.loads(r.text), indent = 2)) 64 | assert r.ok and r.status_code == 200 65 | 66 | return r.text -------------------------------------------------------------------------------- /contracts/OnChainPricingMainnetLenient.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0 2 | pragma solidity 0.8.10; 3 | 4 | 5 | import {IERC20} from "@oz/token/ERC20/IERC20.sol"; 6 | import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol"; 7 | 8 | 9 | import "../interfaces/uniswap/IUniswapRouterV2.sol"; 10 | import "../interfaces/curve/ICurveRouter.sol"; 11 | 12 | import {OnChainPricingMainnet} from "./OnChainPricingMainnet.sol"; 13 | 14 | 15 | 16 | /// @title OnChainPricing 17 | /// @author Alex the Entreprenerd @ BadgerDAO 18 | /// @dev Mainnet Version of Price Quoter, hardcoded for more efficiency 19 | /// @notice To spin a variant, just change the constants and use the Component Functions at the end of the file 20 | /// @notice Instead of upgrading in the future, just point to a new implementation 21 | /// @notice This version has 5% extra slippage to allow further flexibility 22 | /// if the manager abuses the check you should consider reverting back to a more rigorous pricer 23 | contract OnChainPricingMainnetLenient is OnChainPricingMainnet { 24 | 25 | // === SLIPPAGE === // 26 | // Can change slippage within rational limits 27 | address public constant TECH_OPS = 0x86cbD0ce0c087b482782c181dA8d191De18C8275; 28 | 29 | uint256 private constant MAX_BPS = 10_000; 30 | 31 | uint256 private constant MAX_SLIPPAGE = 500; // 5% 32 | 33 | uint256 public slippage = 200; // 2% Initially 34 | 35 | constructor( 36 | address _uniV3Simulator, 37 | address _balancerV2Simulator 38 | ) OnChainPricingMainnet(_uniV3Simulator, _balancerV2Simulator){ 39 | // Silence is golden 40 | } 41 | 42 | function setSlippage(uint256 newSlippage) external { 43 | require(msg.sender == TECH_OPS, "Only TechOps"); 44 | require(newSlippage < MAX_SLIPPAGE); 45 | slippage = newSlippage; 46 | } 47 | 48 | // === PRICING === // 49 | 50 | /// @dev View function for testing the routing of the strategy 51 | function findOptimalSwap(address tokenIn, address tokenOut, uint256 amountIn) external view override returns (Quote memory q) { 52 | q = _findOptimalSwap(tokenIn, tokenOut, amountIn); 53 | q.amountOut = q.amountOut * (MAX_BPS - slippage) / MAX_BPS; 54 | } 55 | } -------------------------------------------------------------------------------- /tests/aura_processor/test_real_life_revert.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | from brownie import * 3 | import pytest 4 | 5 | LIVE_PROCESSOR = "0xC66C9F87d847dDc964724B789D3113885b08efCF" 6 | 7 | @pytest.fixture 8 | def reverting_contract(): 9 | return AuraBribesProcessor.at(LIVE_PROCESSOR) 10 | 11 | 12 | @pytest.fixture 13 | def live_pricer(reverting_contract): 14 | return reverting_contract.pricer() 15 | 16 | @pytest.fixture 17 | def fixed_contract(live_pricer, reverting_contract, manager, aura, aura_whale): 18 | 19 | ## Deploy 20 | p = AuraBribesProcessor.deploy(live_pricer, {"from": manager}) 21 | 22 | 23 | amount_to_test = aura.balanceOf(reverting_contract) 24 | 25 | ## Send some aura 26 | aura.transfer(p, amount_to_test, {"from": aura_whale}) 27 | 28 | return p 29 | 30 | 31 | 32 | def test_compare_live_to_fix(reverting_contract, fixed_contract, manager, aura, bve_aura): 33 | """ 34 | Demonstrate revert in old code 35 | Shows new code won't revert 36 | Proves math equivalence via exact amount sent to tree 37 | """ 38 | live_manager = accounts.at(reverting_contract.manager(), force=True) 39 | 40 | ## Setup 41 | tree = fixed_contract.BADGER_TREE() 42 | prev_bal = bve_aura.balanceOf(tree) 43 | 44 | bve_sett = interface.ISettV4(fixed_contract.BVE_AURA()) 45 | 46 | ## Math for expected number of tokens out from vault 47 | prev_bal = bve_aura.balanceOf(tree) 48 | 49 | aura_to_process = aura.balanceOf(fixed_contract) 50 | 51 | assert aura_to_process > 0 52 | 53 | ops_fee = aura_to_process * fixed_contract.OPS_FEE() // fixed_contract.MAX_BPS() 54 | 55 | expected_bve_aura_to_tree = (aura_to_process - ops_fee) * bve_sett.totalSupply() // bve_sett.balance() 56 | 57 | 58 | ## Show revert on live ## NOTE: This may stop reverting for external reasons 59 | with brownie.reverts(): 60 | reverting_contract.swapAURATobveAURAAndEmit({"from": live_manager}) 61 | 62 | 63 | 64 | ## Run fixed and check 65 | fixed_contract.swapAURATobveAURAAndEmit({"from": manager}) 66 | after_bal = bve_aura.balanceOf(tree) 67 | 68 | ## Net increase 69 | assert after_bal > prev_bal 70 | 71 | ## Proper exact math to make sure no value was leaked 72 | assert after_bal - prev_bal == expected_bve_aura_to_tree 73 | 74 | -------------------------------------------------------------------------------- /tests/bribes_processor/test_sell_bribes_weth.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | from brownie import * 3 | from scripts.send_order import get_cowswap_order 4 | 5 | """ 6 | sellBribeForWeth 7 | Can't sell badger 8 | Can't sell CVX 9 | Works for only X to ETH 10 | """ 11 | 12 | 13 | ### Sell Bribes for Weth 14 | 15 | def test_sell_bribes_for_weth_cant_sell_badger(setup_processor, badger, weth, manager): 16 | sell_amount = 10000000000000000000 17 | 18 | order_details = get_cowswap_order(setup_processor, badger, weth, sell_amount) 19 | 20 | data = order_details.order_data 21 | uid = order_details.order_uid 22 | 23 | with brownie.reverts(): 24 | setup_processor.sellBribeForWeth(data, uid, {"from": manager}) 25 | 26 | def test_sell_bribes_for_weth_cant_sell_cvx(setup_processor, cvx, weth, manager): 27 | sell_amount = 10000000000000000000 28 | 29 | order_details = get_cowswap_order(setup_processor, cvx, weth, sell_amount) 30 | 31 | data = order_details.order_data 32 | uid = order_details.order_uid 33 | 34 | with brownie.reverts(): 35 | setup_processor.sellBribeForWeth(data, uid, {"from": manager}) 36 | 37 | 38 | def test_sell_bribes_for_weth_works_when_selling_usdc_for_weth(setup_processor, usdc, weth, manager, settlement): 39 | sell_amount = 1000000000 40 | 41 | order_details = get_cowswap_order(setup_processor, usdc, weth, sell_amount) 42 | 43 | data = order_details.order_data 44 | uid = order_details.order_uid 45 | 46 | setup_processor.sellBribeForWeth(data, uid, {"from": manager}) 47 | 48 | assert settlement.preSignature(uid) > 0 49 | 50 | def test_sell_bribes_for_weth_must_buy_weth_cant_sell_weth(setup_processor, usdc, weth, cvx, manager, settlement): 51 | """ 52 | Must buy WETH 53 | Can't sell WETH 54 | """ 55 | sell_amount = 100000000000000000000 56 | 57 | order_details = get_cowswap_order(setup_processor, weth, usdc, sell_amount) 58 | 59 | data = order_details.order_data 60 | uid = order_details.order_uid 61 | 62 | with brownie.reverts(): 63 | setup_processor.sellBribeForWeth(data, uid, {"from": manager}) 64 | 65 | 66 | order_details = get_cowswap_order(setup_processor, weth, usdc, sell_amount) 67 | 68 | data = order_details.order_data 69 | uid = order_details.order_uid 70 | 71 | data[1] = weth.address ## Data.sellToken 72 | 73 | ## Fails because UID is also invalid (Can't get API order of WETH-WETH) 74 | with brownie.reverts(): 75 | setup_processor.sellBribeForWeth(data, uid, {"from": manager}) 76 | 77 | 78 | -------------------------------------------------------------------------------- /tests/aura_processor/test_sell_bribes_weth_aura.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | from brownie import * 3 | from scripts.send_order import get_cowswap_order 4 | 5 | """ 6 | sellBribeForWeth 7 | Can't sell badger 8 | Can't sell AURA 9 | Works for only X to ETH 10 | """ 11 | 12 | 13 | ### Sell Bribes for Weth 14 | 15 | def test_sell_bribes_for_weth_cant_sell_badger(setup_aura_processor, badger, weth, manager): 16 | sell_amount = 10000000000000000000 17 | 18 | order_details = get_cowswap_order(setup_aura_processor, badger, weth, sell_amount) 19 | 20 | data = order_details.order_data 21 | uid = order_details.order_uid 22 | 23 | with brownie.reverts(): 24 | setup_aura_processor.sellBribeForWeth(data, uid, {"from": manager}) 25 | 26 | def test_sell_bribes_for_weth_cant_sell_aura(setup_aura_processor, weth, manager, aura): 27 | sell_amount = 10000000000000000000000 28 | 29 | order_details = get_cowswap_order(setup_aura_processor, aura, weth, sell_amount) 30 | 31 | data = order_details.order_data 32 | uid = order_details.order_uid 33 | 34 | with brownie.reverts(): 35 | setup_aura_processor.sellBribeForWeth(data, uid, {"from": manager}) 36 | 37 | 38 | def test_sell_bribes_for_weth_works_when_selling_usdc_for_weth(setup_aura_processor, usdc, weth, manager, settlement): 39 | ## NOTE: Because of UniV3 this may be detected as a revert, run the test separately and it should pass 40 | sell_amount = 1000000000 41 | 42 | order_details = get_cowswap_order(setup_aura_processor, usdc, weth, sell_amount) 43 | 44 | data = order_details.order_data 45 | uid = order_details.order_uid 46 | 47 | setup_aura_processor.sellBribeForWeth(data, uid, {"from": manager}) 48 | 49 | assert settlement.preSignature(uid) > 0 50 | 51 | def test_sell_bribes_for_weth_must_buy_weth_cant_sell_weth(setup_aura_processor, usdc, weth, aura, manager, settlement): 52 | """ 53 | Must buy WETH 54 | Can't sell WETH 55 | """ 56 | sell_amount = 100000000000000000000 57 | 58 | order_details = get_cowswap_order(setup_aura_processor, weth, usdc, sell_amount) 59 | 60 | data = order_details.order_data 61 | uid = order_details.order_uid 62 | 63 | with brownie.reverts(): 64 | setup_aura_processor.sellBribeForWeth(data, uid, {"from": manager}) 65 | 66 | 67 | data[1] = weth.address ## Data.sellToken 68 | 69 | ## Fails because UID is also invalid (Can't get API order of WETH-WETH) 70 | with brownie.reverts(): 71 | setup_aura_processor.sellBribeForWeth(data, uid, {"from": manager}) 72 | 73 | 74 | -------------------------------------------------------------------------------- /interfaces/uniswap/IV3Quoter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0-or-later 2 | pragma solidity >=0.7.5; 3 | pragma abicoder v2; 4 | 5 | /// @title Quoter Interface 6 | /// @notice Supports quoting the calculated amounts from exact input or exact output swaps 7 | /// @dev These functions are not marked view because they rely on calling non-view functions and reverting 8 | /// to compute the result. They are also not gas efficient and should not be called on-chain. 9 | interface IV3Quoter { 10 | /// @notice Returns the amount out received for a given exact input swap without executing the swap 11 | /// @param path The path of the swap, i.e. each token pair and the pool fee 12 | /// @param amountIn The amount of the first token to swap 13 | /// @return amountOut The amount of the last token that would be received 14 | function quoteExactInput(bytes memory path, uint256 amountIn) external returns (uint256 amountOut); 15 | 16 | /// @notice Returns the amount out received for a given exact input but for a swap of a single pool 17 | /// @param tokenIn The token being swapped in 18 | /// @param tokenOut The token being swapped out 19 | /// @param fee The fee of the token pool to consider for the pair 20 | /// @param amountIn The desired input amount 21 | /// @param sqrtPriceLimitX96 The price limit of the pool that cannot be exceeded by the swap 22 | /// @return amountOut The amount of `tokenOut` that would be received 23 | function quoteExactInputSingle( 24 | address tokenIn, 25 | address tokenOut, 26 | uint24 fee, 27 | uint256 amountIn, 28 | uint160 sqrtPriceLimitX96 29 | ) external returns (uint256 amountOut); 30 | 31 | /// @notice Returns the amount in required for a given exact output swap without executing the swap 32 | /// @param path The path of the swap, i.e. each token pair and the pool fee 33 | /// @param amountOut The amount of the last token to receive 34 | /// @return amountIn The amount of first token required to be paid 35 | function quoteExactOutput(bytes memory path, uint256 amountOut) external returns (uint256 amountIn); 36 | 37 | /// @notice Returns the amount in required to receive the given exact output amount but for a swap of a single pool 38 | /// @param tokenIn The token being swapped in 39 | /// @param tokenOut The token being swapped out 40 | /// @param fee The fee of the token pool to consider for the pair 41 | /// @param amountOut The desired output amount 42 | /// @param sqrtPriceLimitX96 The price limit of the pool that cannot be exceeded by the swap 43 | /// @return amountIn The amount required as the input for the swap in order to receive `amountOut` 44 | function quoteExactOutputSingle( 45 | address tokenIn, 46 | address tokenOut, 47 | uint24 fee, 48 | uint256 amountOut, 49 | uint160 sqrtPriceLimitX96 50 | ) external returns (uint256 amountIn); 51 | } -------------------------------------------------------------------------------- /tests/on_chain_pricer/test_bribe_tokens_supported.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from brownie import * 3 | 4 | USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" 5 | 6 | 7 | ## Mostly Aura 8 | AURA = "0xC0c293ce456fF0ED870ADd98a0828Dd4d2903DBF" 9 | AURA_BAL = "0x616e8BfA43F920657B3497DBf40D6b1A02D4608d" 10 | 11 | BADGER = "0x3472A5A71965499acd81997a54BBA8D852C6E53d" 12 | 13 | SD = "0x30d20208d987713f46dfd34ef128bb16c404d10f" ## Pretty much completely new token https://etherscan.io/token/0x30d20208d987713f46dfd34ef128bb16c404d10f#balances 14 | 15 | DFX = "0x888888435FDe8e7d4c54cAb67f206e4199454c60" ## Fairly Liquid: https://etherscan.io/token/0x888888435FDe8e7d4c54cAb67f206e4199454c60#balances 16 | 17 | FDT = "0xEd1480d12bE41d92F36f5f7bDd88212E381A3677" ## Illiquid as of today, in vault but no pool I could find https://etherscan.io/token/0xEd1480d12bE41d92F36f5f7bDd88212E381A3677#balances 18 | 19 | LDO = "0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32" 20 | COW = "0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB" ## Has pair with GNO and with WETH 21 | GNO = "0x6810e776880C02933D47DB1b9fc05908e5386b96" 22 | 23 | ## Mostly Votium 24 | CVX = "0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B" 25 | WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" 26 | SNX = "0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F" 27 | TRIBE = "0xc7283b66Eb1EB5FB86327f08e1B5816b0720212B" 28 | FLX = "0x6243d8cea23066d098a15582d81a598b4e8391f4" 29 | INV = "0x41d5d79431a913c4ae7d69a668ecdfe5ff9dfb68" 30 | FXS = "0x3432B6A60D23Ca0dFCa7761B7ab56459D9C964D0" 31 | 32 | 33 | ## More Random Votium stuff 34 | TUSD = "0x0000000000085d4780B73119b644AE5ecd22b376" 35 | STG = "0xAf5191B0De278C7286d6C7CC6ab6BB8A73bA2Cd6" 36 | LYRA = "0x01BA67AAC7f75f647D94220Cc98FB30FCc5105Bf" 37 | JPEG = "0xE80C0cd204D654CEbe8dd64A4857cAb6Be8345a3" 38 | GRO = "0x3Ec8798B81485A254928B70CDA1cf0A2BB0B74D7" 39 | EURS = "0xdB25f211AB05b1c97D595516F45794528a807ad8" 40 | 41 | ## New Aura Pools 42 | DIGG = "0x798D1bE841a82a273720CE31c822C61a67a601C3" 43 | GRAVI_AURA = "0xBA485b556399123261a5F9c95d413B4f93107407" 44 | 45 | TOKENS_18_DECIMALS = [ 46 | USDC, 47 | AURA, 48 | AURA_BAL, 49 | BADGER, 50 | SD, ## Not Supported -> Cannot fix at this time 51 | DFX, 52 | FDT, ## Not Supported -> Cannot fix at this time 53 | LDO, 54 | COW, 55 | GNO, 56 | CVX, 57 | SNX, 58 | TRIBE, 59 | FLX, 60 | INV, 61 | FXS, 62 | 63 | ## More Coins 64 | TUSD, 65 | STG, 66 | LYRA, 67 | JPEG, 68 | GRO, 69 | EURS, 70 | 71 | ## From new Balancer Pools 72 | DIGG, 73 | GRAVI_AURA 74 | ] 75 | 76 | @pytest.mark.parametrize("token", TOKENS_18_DECIMALS) 77 | def test_are_bribes_supported(pricerwrapper, token): 78 | pricer = pricerwrapper 79 | """ 80 | Given a bunch of tokens historically used as bribes, verifies the pricer will return non-zero value 81 | We sell all to WETH which is pretty realistic 82 | """ 83 | 84 | ## 1e18 for everything, even with insane slippage will still return non-zero which is sufficient at this time 85 | AMOUNT = 1e18 86 | 87 | res = pricer.isPairSupported(token, WETH, AMOUNT) 88 | assert res 89 | 90 | quote = pricer.findOptimalSwap(token, WETH, AMOUNT) 91 | assert quote[1][1] > 0 92 | 93 | -------------------------------------------------------------------------------- /contracts/libraries/uniswap/BitMath.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.7.6; 3 | pragma abicoder v2; 4 | 5 | // https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/BitMath.sol 6 | library BitMath { 7 | /// @notice Returns the index of the most significant bit of the number, 8 | /// where the least significant bit is at index 0 and the most significant bit is at index 255 9 | /// @dev The function satisfies the property: 10 | /// x >= 2**mostSignificantBit(x) and x < 2**(mostSignificantBit(x)+1) 11 | /// @param x the value for which to compute the most significant bit, must be greater than 0 12 | /// @return r the index of the most significant bit 13 | function mostSignificantBit(uint256 x) internal pure returns (uint8 r) { 14 | require(x > 0); 15 | 16 | if (x >= 0x100000000000000000000000000000000) { 17 | x >>= 128; 18 | r += 128; 19 | } 20 | if (x >= 0x10000000000000000) { 21 | x >>= 64; 22 | r += 64; 23 | } 24 | if (x >= 0x100000000) { 25 | x >>= 32; 26 | r += 32; 27 | } 28 | if (x >= 0x10000) { 29 | x >>= 16; 30 | r += 16; 31 | } 32 | if (x >= 0x100) { 33 | x >>= 8; 34 | r += 8; 35 | } 36 | if (x >= 0x10) { 37 | x >>= 4; 38 | r += 4; 39 | } 40 | if (x >= 0x4) { 41 | x >>= 2; 42 | r += 2; 43 | } 44 | if (x >= 0x2) r += 1; 45 | } 46 | 47 | /// @notice Returns the index of the least significant bit of the number, 48 | /// where the least significant bit is at index 0 and the most significant bit is at index 255 49 | /// @dev The function satisfies the property: 50 | /// (x & 2**leastSignificantBit(x)) != 0 and (x & (2**(leastSignificantBit(x)) - 1)) == 0) 51 | /// @param x the value for which to compute the least significant bit, must be greater than 0 52 | /// @return r the index of the least significant bit 53 | function leastSignificantBit(uint256 x) internal pure returns (uint8 r) { 54 | require(x > 0); 55 | 56 | r = 255; 57 | if (x & type(uint128).max > 0) { 58 | r -= 128; 59 | } else { 60 | x >>= 128; 61 | } 62 | if (x & type(uint64).max > 0) { 63 | r -= 64; 64 | } else { 65 | x >>= 64; 66 | } 67 | if (x & type(uint32).max > 0) { 68 | r -= 32; 69 | } else { 70 | x >>= 32; 71 | } 72 | if (x & type(uint16).max > 0) { 73 | r -= 16; 74 | } else { 75 | x >>= 16; 76 | } 77 | if (x & type(uint8).max > 0) { 78 | r -= 8; 79 | } else { 80 | x >>= 8; 81 | } 82 | if (x & 0xf > 0) { 83 | r -= 4; 84 | } else { 85 | x >>= 4; 86 | } 87 | if (x & 0x3 > 0) { 88 | r -= 2; 89 | } else { 90 | x >>= 2; 91 | } 92 | if (x & 0x1 > 0) r -= 1; 93 | } 94 | } -------------------------------------------------------------------------------- /contracts/libraries/balancer/BalancerQuoter.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.7.6; 3 | pragma abicoder v2; 4 | 5 | enum BalancerV2SwapKind { GIVEN_IN, GIVEN_OUT } 6 | 7 | struct BalancerV2BatchSwapStep { 8 | bytes32 poolId; 9 | uint256 assetInIndex; 10 | uint256 assetOutIndex; 11 | uint256 amount; 12 | bytes userData; 13 | } 14 | 15 | struct BalancerV2FundManagement { 16 | address sender; 17 | bool fromInternalBalance; 18 | address recipient; 19 | bool toInternalBalance; 20 | } 21 | 22 | interface IBalancerV2VaultQuoter { 23 | function queryBatchSwap(BalancerV2SwapKind kind, BalancerV2BatchSwapStep[] calldata swaps, address[] calldata assets, BalancerV2FundManagement calldata funds) external returns (int256[] memory assetDeltas); 24 | } 25 | 26 | // gas consuming quoter https://dev.balancer.fi/resources/query-how-much-x-for-y 27 | library BalancerQuoter { 28 | address private constant _vault = 0xBA12222222228d8Ba445958a75a0704d566BF2C8; 29 | 30 | function getBalancerPriceWithinPool(bytes32 poolId, address tokenIn, uint256 amountIn, address tokenOut) public returns (uint256) { 31 | 32 | address[] memory assets = new address[](2); 33 | assets[0] = tokenIn; 34 | assets[1] = tokenOut; 35 | 36 | BalancerV2BatchSwapStep[] memory swaps = new BalancerV2BatchSwapStep[](1); 37 | swaps[0] = BalancerV2BatchSwapStep(poolId, 0, 1, amountIn, ""); 38 | 39 | BalancerV2FundManagement memory funds = BalancerV2FundManagement(address(this), false, address(this), false); 40 | 41 | int256[] memory assetDeltas = IBalancerV2VaultQuoter(_vault).queryBatchSwap(BalancerV2SwapKind.GIVEN_IN, swaps, assets, funds); 42 | 43 | // asset deltas: either transferring assets from the sender (for positive deltas) or to the recipient (for negative deltas). 44 | return assetDeltas.length > 0 ? uint256(0 - assetDeltas[assetDeltas.length - 1]) : 0; 45 | } 46 | 47 | /// @dev Given the input/output/connector token, returns the quote for input amount from Balancer V2 48 | function getBalancerPriceWithConnector(bytes32 firstPoolId, bytes32 secondPoolId, address tokenIn, uint256 amountIn, address tokenOut, address connectorToken) public returns (uint256) { 49 | address[] memory assets = new address[](3); 50 | assets[0] = tokenIn; 51 | assets[1] = connectorToken; 52 | assets[2] = tokenOut; 53 | 54 | BalancerV2BatchSwapStep[] memory swaps = new BalancerV2BatchSwapStep[](2); 55 | swaps[0] = BalancerV2BatchSwapStep(firstPoolId, 0, 1, amountIn, ""); 56 | swaps[1] = BalancerV2BatchSwapStep(secondPoolId, 1, 2, 0, "");// amount == 0 means use all from previous step 57 | 58 | BalancerV2FundManagement memory funds = BalancerV2FundManagement(address(this), false, address(this), false); 59 | 60 | int256[] memory assetDeltas = IBalancerV2VaultQuoter(_vault).queryBatchSwap(BalancerV2SwapKind.GIVEN_IN, swaps, assets, funds); 61 | 62 | // asset deltas: either transferring assets from the sender (for positive deltas) or to the recipient (for negative deltas). 63 | return assetDeltas.length > 0 ? uint256(0 - assetDeltas[assetDeltas.length - 1]) : 0; 64 | } 65 | } -------------------------------------------------------------------------------- /interfaces/uniswap/IUniswapRouterV2.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.5.0; 3 | 4 | interface IUniswapRouterV2 { 5 | function factory() external view returns (address); 6 | 7 | function addLiquidity( 8 | address tokenA, 9 | address tokenB, 10 | uint256 amountADesired, 11 | uint256 amountBDesired, 12 | uint256 amountAMin, 13 | uint256 amountBMin, 14 | address to, 15 | uint256 deadline 16 | ) 17 | external 18 | returns ( 19 | uint256 amountA, 20 | uint256 amountB, 21 | uint256 liquidity 22 | ); 23 | 24 | function addLiquidityETH( 25 | address token, 26 | uint256 amountTokenDesired, 27 | uint256 amountTokenMin, 28 | uint256 amountETHMin, 29 | address to, 30 | uint256 deadline 31 | ) 32 | external 33 | payable 34 | returns ( 35 | uint256 amountToken, 36 | uint256 amountETH, 37 | uint256 liquidity 38 | ); 39 | 40 | function removeLiquidity( 41 | address tokenA, 42 | address tokenB, 43 | uint256 liquidity, 44 | uint256 amountAMin, 45 | uint256 amountBMin, 46 | address to, 47 | uint256 deadline 48 | ) external returns (uint256 amountA, uint256 amountB); 49 | 50 | function getAmountsOut(uint256 amountIn, address[] calldata path) 51 | external 52 | view 53 | returns (uint256[] memory amounts); 54 | 55 | function getAmountsIn(uint256 amountOut, address[] calldata path) 56 | external 57 | view 58 | returns (uint256[] memory amounts); 59 | 60 | function swapETHForExactTokens( 61 | uint256 amountOut, 62 | address[] calldata path, 63 | address to, 64 | uint256 deadline 65 | ) external payable returns (uint256[] memory amounts); 66 | 67 | function swapExactETHForTokens( 68 | uint256 amountOutMin, 69 | address[] calldata path, 70 | address to, 71 | uint256 deadline 72 | ) external payable returns (uint256[] memory amounts); 73 | 74 | function swapExactTokensForETH( 75 | uint256 amountIn, 76 | uint256 amountOutMin, 77 | address[] calldata path, 78 | address to, 79 | uint256 deadline 80 | ) external returns (uint256[] memory amounts); 81 | 82 | function swapTokensForExactETH( 83 | uint256 amountOut, 84 | uint256 amountInMax, 85 | address[] calldata path, 86 | address to, 87 | uint256 deadline 88 | ) external returns (uint256[] memory amounts); 89 | 90 | function swapExactTokensForTokens( 91 | uint256 amountIn, 92 | uint256 amountOutMin, 93 | address[] calldata path, 94 | address to, 95 | uint256 deadline 96 | ) external returns (uint256[] memory amounts); 97 | 98 | function swapTokensForExactTokens( 99 | uint256 amountOut, 100 | uint256 amountInMax, 101 | address[] calldata path, 102 | address to, 103 | uint256 deadline 104 | ) external returns (uint256[] memory amounts); 105 | } 106 | -------------------------------------------------------------------------------- /tests/gas_benchmark/benchmark_token_coverage.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | from brownie import * 3 | import pytest 4 | 5 | """ 6 | Benchmark test for token coverage in findOptimalSwap with focus in DeFi category 7 | Selected tokens from https://defillama.com/chain/Ethereum 8 | This file is ok to be exclcuded in test suite due to its underluying functionality should be covered by other tests 9 | Rename the file to test_benchmark_token_coverage.py to make this part of the testing suite if required 10 | """ 11 | 12 | TOP_DECIMAL18_TOKENS = [ 13 | ("0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2", 100), # MKR 14 | ("0x5a98fcbea516cf06857215779fd812ca3bef1b32", 10000), # LDO 15 | ("0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", 10000), # UNI 16 | ("0xd533a949740bb3306d119cc777fa900ba034cd52", 10000), # CRV 17 | ("0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9", 1000), # AAVE 18 | ("0x4e3fbd56cd56c3e72c1403e103b45db9da5b9d2b", 10000), # CVX 19 | ("0xc00e94cb662c3520282e6f5717214004a7f26888", 1000), # COMP 20 | ("0x6f40d4A6237C257fff2dB00FA0510DeEECd303eb", 10000), # INST 21 | ("0xba100000625a3754423978a60c9317c58a424e3D", 10000), # BAL 22 | ("0x3432b6a60d23ca0dfca7761b7ab56459d9c964d0", 10000), # FXS 23 | ("0x6b3595068778dd592e39a122f4f5a5cf09c90fe2", 10000), # SUSHI 24 | ("0x92D6C1e31e14520e676a687F0a93788B716BEff5", 10000), # DYDX 25 | ("0x0bc529c00c6401aef6d220be8c6ea1667f6ad93e", 10), # YFI 26 | ("0x6DEA81C8171D0bA574754EF6F8b412F2Ed88c54D", 50000), # LQTY 27 | ("0xd33526068d116ce69f19a9ee46f0bd304f21a51f", 1000), # RPL 28 | ("0x090185f2135308bad17527004364ebcc2d37e5f6", 10000000), # SPELL 29 | ("0x77777feddddffc19ff86db637967013e6c6a116c", 1000), # TORN 30 | ("0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f", 10000), # SNX 31 | ("0x0d438f3b5175bebc262bf23753c1e53d03432bde", 1000), # WNXM 32 | ("0xff20817765cb7f73d4bde2e66e067e58d11095c2", 10000000), # AMP 33 | ("0xd9Fcd98c322942075A5C3860693e9f4f03AAE07b", 1000), # EUL 34 | ("0x1f573d6fb3f13d689ff844b4ce37794d79a7ff1c", 50000), # BNT 35 | ("0xdbdb4d16eda451d0503b854cf79d55697f90c8df", 1000), # ALCX 36 | ("0x73968b9a57c6e53d41345fd57a6e6ae27d6cdb2f", 50000), # SDT 37 | ("0x31429d1856ad1377a8a0079410b297e1a9e214c2", 1000000), # ANGLE 38 | ("0x04Fa0d235C4abf4BcF4787aF4CF447DE572eF828", 10000), # UMA 39 | ("0x6123B0049F904d730dB3C36a31167D9d4121fA6B", 50000), # RBN 40 | ("0x956F47F50A910163D8BF957Cf5846D573E7f87CA", 10000), # FEI 41 | ("0x853d955acef822db058eb8505911ed77f175b99e", 10000), # FRAX 42 | ("0xD291E7a03283640FDc51b121aC401383A46cC623", 10000), # RGT 43 | ("0x1b40183efb4dd766f11bda7a7c3ad8982e998421", 50000), # VSP 44 | ("0x0cec1a9154ff802e7934fc916ed7ca50bde6844e", 50000), # POOL 45 | ("0x43dfc4159d86f3a37a5a4b3d4580b888ad7d4ddd", 50000), # DODO 46 | ("0xe28b3b32b6c345a34ff64674606124dd5aceca30", 10000), # INJ 47 | ("0x0f2d719407fdbeff09d87557abb7232601fd9f29", 10000), # SYN 48 | ] 49 | 50 | @pytest.mark.parametrize("token,count", TOP_DECIMAL18_TOKENS) 51 | def test_token_decimal18(oneE18, weth, token, count, pricerwrapper): 52 | pricer = pricerwrapper 53 | sell_token = token 54 | ## 1e18 55 | sell_count = count 56 | sell_amount = sell_count * oneE18 ## 1e18 57 | 58 | quote = pricer.findOptimalSwap(sell_token, weth.address, sell_amount) 59 | assert quote[1][1] > 0 60 | -------------------------------------------------------------------------------- /tests/bribes_processor/test_swap_weth_badger_cvx.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | from brownie import * 3 | from scripts.send_order import get_cowswap_order 4 | 5 | """ 6 | swapWethForBadger 7 | Works 8 | Reverts if not weth -> badger 9 | 10 | swapWethForCVX 11 | Works 12 | Reverts if not weth -> CVX 13 | """ 14 | 15 | 16 | ### Swap Weth for Badger 17 | 18 | def test_swap_weth_for_badger(setup_processor, weth, badger, manager, settlement): 19 | sell_amount = 10000000000000000000 20 | 21 | order_details = get_cowswap_order(setup_processor, weth, badger, sell_amount) 22 | 23 | data = order_details.order_data 24 | uid = order_details.order_uid 25 | 26 | setup_processor.swapWethForBadger(data, uid, {"from": manager}) 27 | 28 | assert settlement.preSignature(uid) > 0 29 | 30 | 31 | def test_swap_weth_for_badger_must_be_weth_badger(setup_processor, weth, badger, usdc, cvx, manager, settlement): 32 | ## Fail if opposite swap 33 | sell_amount = 10000000000000000000 34 | 35 | order_details = get_cowswap_order(setup_processor, badger, weth, sell_amount) 36 | 37 | data = order_details.order_data 38 | uid = order_details.order_uid 39 | 40 | with brownie.reverts(): 41 | setup_processor.swapWethForBadger(data, uid, {"from": manager}) 42 | 43 | ## Fail if selling non weth 44 | order_details = get_cowswap_order(setup_processor, usdc, badger, sell_amount) 45 | 46 | data = order_details.order_data 47 | uid = order_details.order_uid 48 | 49 | with brownie.reverts(): 50 | setup_processor.swapWethForBadger(data, uid, {"from": manager}) 51 | 52 | ## Fail if random token combo 53 | order_details = get_cowswap_order(setup_processor, usdc, cvx, sell_amount) 54 | 55 | data = order_details.order_data 56 | uid = order_details.order_uid 57 | 58 | with brownie.reverts(): 59 | setup_processor.swapWethForBadger(data, uid, {"from": manager}) 60 | 61 | 62 | 63 | ### Swap Weth for CVX 64 | 65 | def test_swap_weth_for_cvx(setup_processor, weth, cvx, manager, settlement): 66 | sell_amount = 100000000000000000000 67 | 68 | order_details = get_cowswap_order(setup_processor, weth, cvx, sell_amount) 69 | 70 | data = order_details.order_data 71 | uid = order_details.order_uid 72 | 73 | setup_processor.swapWethForCVX(data, uid, {"from": manager}) 74 | 75 | assert settlement.preSignature(uid) > 0 76 | 77 | def test_swap_weth_for_cvx_must_be_weth_cvx(setup_processor, weth, badger, usdc, cvx, manager, settlement): 78 | ## Fail if opposite swap 79 | sell_amount = 100000000000000000000 80 | 81 | order_details = get_cowswap_order(setup_processor, cvx, weth, sell_amount) 82 | 83 | data = order_details.order_data 84 | uid = order_details.order_uid 85 | 86 | with brownie.reverts(): 87 | setup_processor.swapWethForCVX(data, uid, {"from": manager}) 88 | 89 | ## Fail if selling non weth 90 | order_details = get_cowswap_order(setup_processor, usdc, cvx, sell_amount) 91 | 92 | data = order_details.order_data 93 | uid = order_details.order_uid 94 | 95 | with brownie.reverts(): 96 | setup_processor.swapWethForCVX(data, uid, {"from": manager}) 97 | 98 | ## Fail if random token combo 99 | order_details = get_cowswap_order(setup_processor, usdc, badger, sell_amount) 100 | 101 | data = order_details.order_data 102 | uid = order_details.order_uid 103 | 104 | with brownie.reverts(): 105 | setup_processor.swapWethForCVX(data, uid, {"from": manager}) 106 | -------------------------------------------------------------------------------- /tests/on_chain_pricer/test_univ3_pricer.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | from brownie import * 3 | import pytest 4 | 5 | """ 6 | test case for COW token to fix reported issue https://github.com/GalloDaSballo/fair-selling/issues/26 7 | """ 8 | def test_get_univ3_price_cow(oneE18, weth, usdc_whale, pricer): 9 | ## 1e18 10 | token = "0xdef1ca1fb7fbcdc777520aa7f396b4e015f497ab" 11 | sell_count = 12209 12 | sell_amount = sell_count * oneE18 13 | 14 | ## minimum quote for COW in ETH(1e18) 15 | quoteInV2 = pricer.getUniPrice(pricer.UNIV2_ROUTER(), weth.address, token, sell_amount) 16 | assert quoteInV2 == 0 17 | p = sell_count * 0.00005 * oneE18 18 | quote = pricer.simulateUniV3Swap(weth.address, sell_amount, token, 10000, False, "0xFCfDFC98062d13a11cec48c44E4613eB26a34293") 19 | assert quote >= p 20 | 21 | ## check against quoter 22 | quoterP = interface.IV3Quoter(pricer.UNIV3_QUOTER()).quoteExactInputSingle.call(token, weth.address, 10000, sell_amount, 0, {'from': usdc_whale.address}) 23 | assert quoterP == quote 24 | 25 | """ 26 | getUniV3Price quote for token A swapped to token B directly: A - > B 27 | """ 28 | def test_get_univ3_price_in_range(oneE18, weth, usdc, usdc_whale, pricer): 29 | ## 1e18 30 | sell_count = 1 31 | sell_amount = sell_count * oneE18 32 | 33 | ## minimum quote for ETH in USDC(1e6) ## Rip ETH price 34 | p = sell_count * 900 * 1000000 35 | quoteInRange = pricer.checkUniV3InRangeLiquidity(usdc.address, weth.address, sell_amount, 500, False, "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640") 36 | assert quoteInRange[1] >= p 37 | 38 | ## check against quoter 39 | quoterP = interface.IV3Quoter(pricer.UNIV3_QUOTER()).quoteExactInputSingle.call(weth.address, usdc.address, 500, sell_amount, 0, {'from': usdc_whale.address}) 40 | assert quoterP == quoteInRange[1] 41 | 42 | """ 43 | getUniV3Price quote for token A swapped to token B directly: A - > B 44 | """ 45 | def test_get_univ3_price_cross_tick(oneE18, weth, usdc, usdc_whale, pricer): 46 | ## 1e18 47 | sell_count = 2000 48 | sell_amount = sell_count * oneE18 49 | 50 | ## minimum quote for ETH in USDC(1e6) ## Rip ETH price 51 | p = sell_count * 900 * 1000000 52 | quoteCrossTicks = pricer.simulateUniV3Swap(usdc.address, sell_amount, weth.address, 500, False, "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640") 53 | assert quoteCrossTicks >= p 54 | 55 | ## check against quoter 56 | quoterP = interface.IV3Quoter(pricer.UNIV3_QUOTER()).quoteExactInputSingle.call(weth.address, usdc.address, 500, sell_amount, 0, {'from': usdc_whale.address}) 57 | assert (abs(quoterP - quoteCrossTicks) / quoterP) <= 0.0015 ## thousandsth in quote diff for a millions-dollar-worth swap 58 | 59 | """ 60 | getUniV3PriceWithConnector quote for token A swapped to token B with connector token C: A -> C -> B 61 | """ 62 | def test_get_univ3_price_with_connector(oneE18, wbtc, usdc, weth, dai, pricer): 63 | ## 1e8 64 | sell_amount = 100 * 100000000 65 | 66 | ## minimum quote for WBTC in USDC(1e6) 67 | p = 100 * 15000 * 1000000 68 | assert pricer.sortUniV3Pools(wbtc.address, sell_amount, usdc.address)[0] >= p 69 | 70 | quoteWithConnector = pricer.getUniV3PriceWithConnector(wbtc.address, sell_amount, usdc.address, weth.address) 71 | 72 | ## min price 73 | assert quoteWithConnector >= p 74 | 75 | ## test case for stablecoin DAI -> USDC 76 | daiQuoteWithConnector = pricer.getUniV3PriceWithConnector(dai.address, 10000 * oneE18, usdc.address, weth.address) 77 | assert daiQuoteWithConnector >= 10000 * 0.99 * 1000000 78 | 79 | -------------------------------------------------------------------------------- /interfaces/balancer/WeightedPoolUserData.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0 2 | // This program is free software: you can redistribute it and/or modify 3 | // it under the terms of the GNU General Public License as published by 4 | // the Free Software Foundation, either version 3 of the License, or 5 | // (at your option) any later version. 6 | 7 | // This program is distributed in the hope that it will be useful, 8 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | // GNU General Public License for more details. 11 | 12 | // You should have received a copy of the GNU General Public License 13 | // along with this program. If not, see . 14 | 15 | pragma solidity 0.8.10; 16 | 17 | library WeightedPoolUserData { 18 | // In order to preserve backwards compatibility, make sure new join and exit kinds are added at the end of the enum. 19 | enum JoinKind { 20 | INIT, 21 | EXACT_TOKENS_IN_FOR_BPT_OUT, 22 | TOKEN_IN_FOR_EXACT_BPT_OUT, 23 | ALL_TOKENS_IN_FOR_EXACT_BPT_OUT, 24 | ADD_TOKEN // for Managed Pool 25 | } 26 | enum ExitKind { 27 | EXACT_BPT_IN_FOR_ONE_TOKEN_OUT, 28 | EXACT_BPT_IN_FOR_TOKENS_OUT, 29 | BPT_IN_FOR_EXACT_TOKENS_OUT, 30 | REMOVE_TOKEN // for ManagedPool 31 | } 32 | 33 | function joinKind(bytes memory self) internal pure returns (JoinKind) { 34 | return abi.decode(self, (JoinKind)); 35 | } 36 | 37 | function exitKind(bytes memory self) internal pure returns (ExitKind) { 38 | return abi.decode(self, (ExitKind)); 39 | } 40 | 41 | // Joins 42 | 43 | function initialAmountsIn(bytes memory self) internal pure returns (uint256[] memory amountsIn) { 44 | (, amountsIn) = abi.decode(self, (JoinKind, uint256[])); 45 | } 46 | 47 | function exactTokensInForBptOut(bytes memory self) internal pure returns (uint256[] memory amountsIn, uint256 minBPTAmountOut) { 48 | (, amountsIn, minBPTAmountOut) = abi.decode(self, (JoinKind, uint256[], uint256)); 49 | } 50 | 51 | function tokenInForExactBptOut(bytes memory self) internal pure returns (uint256 bptAmountOut, uint256 tokenIndex) { 52 | (, bptAmountOut, tokenIndex) = abi.decode(self, (JoinKind, uint256, uint256)); 53 | } 54 | 55 | function allTokensInForExactBptOut(bytes memory self) internal pure returns (uint256 bptAmountOut) { 56 | (, bptAmountOut) = abi.decode(self, (JoinKind, uint256)); 57 | } 58 | 59 | function addToken(bytes memory self) internal pure returns (uint256 amountIn) { 60 | (, amountIn) = abi.decode(self, (JoinKind, uint256)); 61 | } 62 | 63 | // Exits 64 | 65 | function exactBptInForTokenOut(bytes memory self) internal pure returns (uint256 bptAmountIn, uint256 tokenIndex) { 66 | (, bptAmountIn, tokenIndex) = abi.decode(self, (ExitKind, uint256, uint256)); 67 | } 68 | 69 | function exactBptInForTokensOut(bytes memory self) internal pure returns (uint256 bptAmountIn) { 70 | (, bptAmountIn) = abi.decode(self, (ExitKind, uint256)); 71 | } 72 | 73 | function bptInForExactTokensOut(bytes memory self) internal pure returns (uint256[] memory amountsOut, uint256 maxBPTAmountIn) { 74 | (, amountsOut, maxBPTAmountIn) = abi.decode(self, (ExitKind, uint256[], uint256)); 75 | } 76 | 77 | // Managed Pool 78 | function removeToken(bytes memory self) internal pure returns (uint256 tokenIndex) { 79 | (, tokenIndex) = abi.decode(self, (ExitKind, uint256)); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /contracts/libraries/uniswap/TickBitmap.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.7.6; 3 | pragma abicoder v2; 4 | 5 | import "./BitMath.sol"; 6 | 7 | interface IUniswapV3PoolBitmap { 8 | function tickBitmap(int16 wordPosition) external view returns (uint256); 9 | } 10 | 11 | struct TickNextWithWordQuery{ 12 | address pool; 13 | int24 tick; 14 | int24 tickSpacing; 15 | bool lte; 16 | } 17 | 18 | // https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/TickBitmap.sol 19 | library TickBitmap { 20 | /// @notice Computes the position in the mapping where the initialized bit for a tick lives 21 | /// @param tick The tick for which to compute the position 22 | /// @return wordPos The key in the mapping containing the word in which the bit is stored 23 | /// @return bitPos The bit position in the word where the flag is stored 24 | function position(int24 tick) private pure returns (int16 wordPos, uint8 bitPos) { 25 | wordPos = int16(tick >> 8); 26 | bitPos = uint8(tick % 256); 27 | } 28 | 29 | /// @notice Returns the next initialized tick contained in the same word (or adjacent word) as the tick that is either 30 | /// to the left (less than or equal to) or right (greater than) of the given tick 31 | /// @param _query.pool The Uniswap V3 pool to fetch the ticks BitMap 32 | /// @param _query.tick The starting tick 33 | /// @param _query.tickSpacing The spacing between usable ticks 34 | /// @param _query.lte Whether to search for the next initialized tick to the left (less than or equal to the starting tick) 35 | /// @return next The next initialized or uninitialized tick up to 256 ticks away from the current tick 36 | /// @return initialized Whether the next tick is initialized, as the function only searches within up to 256 ticks 37 | function nextInitializedTickWithinOneWord(TickNextWithWordQuery memory _query) internal view returns (int24 next, bool initialized) { 38 | int24 compressed = _query.tick / _query.tickSpacing; 39 | if (_query.tick < 0 && _query.tick % _query.tickSpacing != 0) compressed--; // round towards negative infinity 40 | 41 | if (_query.lte) { 42 | (int16 wordPos, uint8 bitPos) = position(compressed); 43 | // all the 1s at or to the right of the current bitPos 44 | uint256 mask = (1 << bitPos) - 1 + (1 << bitPos); 45 | uint256 masked = IUniswapV3PoolBitmap(_query.pool).tickBitmap(wordPos) & mask; 46 | 47 | // if there are no initialized ticks to the right of or at the current tick, return rightmost in the word 48 | initialized = masked != 0; 49 | // overflow/underflow is possible, but prevented externally by limiting both tickSpacing and tick 50 | next = initialized 51 | ? (compressed - int24(bitPos - BitMath.mostSignificantBit(masked))) * _query.tickSpacing 52 | : (compressed - int24(bitPos)) * _query.tickSpacing; 53 | } else { 54 | // start from the word of the next tick, since the current tick state doesn't matter 55 | (int16 wordPos, uint8 bitPos) = position(compressed + 1); 56 | // all the 1s at or to the left of the bitPos 57 | uint256 mask = ~((1 << bitPos) - 1); 58 | uint256 masked = IUniswapV3PoolBitmap(_query.pool).tickBitmap(wordPos) & mask; 59 | 60 | // if there are no initialized ticks to the left of the current tick, return leftmost in the word 61 | initialized = masked != 0; 62 | // overflow/underflow is possible, but prevented externally by limiting both tickSpacing and tick 63 | next = initialized 64 | ? (compressed + 1 + int24(BitMath.leastSignificantBit(masked) - bitPos)) *_query. tickSpacing 65 | : (compressed + 1 + int24(type(uint8).max - bitPos)) * _query.tickSpacing; 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /tests/gas_benchmark/benchmark_pricer_gas.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | from brownie import * 3 | import pytest 4 | 5 | """ 6 | Benchmark test for gas cost in findOptimalSwap on various conditions 7 | This file is ok to be exclcuded in test suite due to its underluying functionality should be covered by other tests 8 | Rename the file to test_benchmark_pricer_gas.py to make this part of the testing suite if required 9 | """ 10 | 11 | def test_gas_only_uniswap_v2(oneE18, weth, pricerwrapper): 12 | pricer = pricerwrapper 13 | token = "0xBC7250C8c3eCA1DfC1728620aF835FCa489bFdf3" # some swap (GM-WETH) only in Uniswap V2 14 | ## 1e18 15 | sell_count = 100000000 16 | sell_amount = sell_count * 1000000000 ## 1e9 17 | 18 | tx = pricer.findOptimalSwap(token, weth.address, sell_amount) 19 | assert tx[1][0] == 1 ## UNIV2 20 | assert tx[1][1] > 0 21 | assert tx[0] <= 80000 ## 73925 in test simulation 22 | 23 | def test_gas_uniswap_v2_sushi(oneE18, weth, pricerwrapper): 24 | pricer = pricerwrapper 25 | token = "0x2e9d63788249371f1DFC918a52f8d799F4a38C94" # some swap (TOKE-WETH) only in Uniswap V2 & SushiSwap 26 | ## 1e18 27 | sell_count = 5000 28 | sell_amount = sell_count * oneE18 ## 1e18 29 | 30 | tx = pricer.findOptimalSwap(token, weth.address, sell_amount) 31 | assert (tx[1][0] == 1 or tx[1][0] == 2) ## UNIV2 or SUSHI 32 | assert tx[1][1] > 0 33 | assert tx[0] <= 90000 ## 83158 in test simulation 34 | 35 | def test_gas_only_balancer_v2(oneE18, weth, aura, pricerwrapper): 36 | pricer = pricerwrapper 37 | token = aura # some swap (AURA-WETH) only in Balancer V2 38 | ## 1e18 39 | sell_count = 8000 40 | sell_amount = sell_count * oneE18 ## 1e18 41 | 42 | tx = pricer.findOptimalSwap(token, weth.address, sell_amount) 43 | assert tx[1][0] == 5 ## BALANCER 44 | assert tx[1][1] > 0 45 | assert tx[0] <= 110000 ## 101190 in test simulation 46 | 47 | def test_gas_only_balancer_v2_with_weth(oneE18, wbtc, aura, pricerwrapper): 48 | pricer = pricerwrapper 49 | token = aura # some swap (AURA-WETH-WBTC) only in Balancer V2 via WETH in between as connector 50 | ## 1e18 51 | sell_count = 8000 52 | sell_amount = sell_count * oneE18 ## 1e18 53 | 54 | tx = pricer.findOptimalSwap(token, wbtc.address, sell_amount) 55 | assert tx[1][0] == 6 ## BALANCERWITHWETH 56 | assert tx[1][1] > 0 57 | assert tx[0] <= 170000 ## 161690 in test simulation 58 | 59 | def test_gas_only_uniswap_v3(oneE18, weth, pricerwrapper): 60 | pricer = pricerwrapper 61 | token = "0xf4d2888d29D722226FafA5d9B24F9164c092421E" # some swap (LOOKS-WETH) only in Uniswap V3 62 | ## 1e18 63 | sell_count = 600000 64 | sell_amount = sell_count * oneE18 ## 1e18 65 | 66 | tx = pricer.findOptimalSwap(token, weth.address, sell_amount) 67 | assert tx[1][0] == 3 ## UNIV3 68 | assert tx[1][1] > 0 69 | assert tx[0] <= 160000 ## 158204 in test simulation 70 | 71 | def test_gas_only_uniswap_v3_with_weth(oneE18, wbtc, pricerwrapper): 72 | pricer = pricerwrapper 73 | token = "0xf4d2888d29D722226FafA5d9B24F9164c092421E" # some swap (LOOKS-WETH-WBTC) only in Uniswap V3 via WETH in between as connector 74 | ## 1e18 75 | sell_count = 600000 76 | sell_amount = sell_count * oneE18 ## 1e18 77 | 78 | tx = pricer.findOptimalSwap(token, wbtc.address, sell_amount) 79 | assert tx[1][0] == 4 ## UNIV3WITHWETH 80 | assert tx[1][1] > 0 81 | assert tx[0] <= 230000 ## 227498 in test simulation 82 | 83 | def test_gas_almost_everything(oneE18, wbtc, weth, pricerwrapper): 84 | pricer = pricerwrapper 85 | token = weth # some swap (WETH-WBTC) almost in every DEX, the most gas-consuming scenario 86 | ## 1e18 87 | sell_count = 10 88 | sell_amount = sell_count * oneE18 ## 1e18 89 | 90 | tx = pricer.findOptimalSwap(token, wbtc.address, sell_amount) 91 | assert (tx[1][0] <= 3 or tx[1][0] == 5) ## CURVE or UNIV2 or SUSHI or UNIV3 or BALANCER 92 | assert tx[1][1] > 0 93 | assert tx[0] <= 210000 ## 200229 in test simulation 94 | -------------------------------------------------------------------------------- /tests/aura_processor/test_swap_weth_badger_aura.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | from brownie import * 3 | from scripts.send_order import get_cowswap_order 4 | 5 | """ 6 | swapWethForBadger 7 | Works 8 | Reverts if not weth -> badger 9 | 10 | swapWethForAURA 11 | Works 12 | Reverts if not weth -> AURA 13 | """ 14 | 15 | 16 | ### Swap Weth for Badger 17 | 18 | def test_swap_weth_for_badger(setup_aura_processor, weth, badger, manager, settlement): 19 | sell_amount = 10000000000000000000 20 | 21 | order_details = get_cowswap_order(setup_aura_processor, weth, badger, sell_amount) 22 | 23 | data = order_details.order_data 24 | uid = order_details.order_uid 25 | 26 | setup_aura_processor.swapWethForBadger(data, uid, {"from": manager}) 27 | 28 | assert settlement.preSignature(uid) > 0 29 | 30 | 31 | def test_swap_weth_for_badger_must_be_weth_badger(setup_aura_processor, weth, badger, usdc, aura, manager, settlement): 32 | ## Fail if opposite swap 33 | sell_amount = 10000000000000000000 34 | 35 | order_details = get_cowswap_order(setup_aura_processor, badger, weth, sell_amount) 36 | 37 | data = order_details.order_data 38 | uid = order_details.order_uid 39 | 40 | with brownie.reverts(): 41 | setup_aura_processor.swapWethForBadger(data, uid, {"from": manager}) 42 | 43 | ## Fail if selling non weth 44 | order_details = get_cowswap_order(setup_aura_processor, usdc, badger, sell_amount) 45 | 46 | data = order_details.order_data 47 | uid = order_details.order_uid 48 | 49 | with brownie.reverts(): 50 | setup_aura_processor.swapWethForBadger(data, uid, {"from": manager}) 51 | 52 | ## Fail if random token combo 53 | order_details = get_cowswap_order(setup_aura_processor, usdc, aura, sell_amount) 54 | 55 | data = order_details.order_data 56 | uid = order_details.order_uid 57 | 58 | with brownie.reverts(): 59 | setup_aura_processor.swapWethForBadger(data, uid, {"from": manager}) 60 | 61 | 62 | 63 | ### Swap Weth for AURA or graviAURA 64 | 65 | def test_swap_weth_for_aura(setup_aura_processor, weth, aura, manager, settlement): 66 | sell_amount = 100000000000000000000 67 | 68 | order_details = get_cowswap_order(setup_aura_processor, weth, aura, sell_amount) 69 | 70 | data = order_details.order_data 71 | uid = order_details.order_uid 72 | 73 | setup_aura_processor.swapWethForAURA(data, uid, {"from": manager}) 74 | 75 | assert settlement.preSignature(uid) > 0 76 | 77 | def test_swap_weth_for_graviaura(setup_aura_processor, weth, bve_aura, manager, settlement): 78 | sell_amount = 10000000000000000000 # 10 wETH since there is lower liquidity for graviAURA (fails with 100 wETH) 79 | 80 | order_details = get_cowswap_order(setup_aura_processor, weth, bve_aura, sell_amount) 81 | 82 | data = order_details.order_data 83 | uid = order_details.order_uid 84 | 85 | setup_aura_processor.swapWethForAURA(data, uid, {"from": manager}) 86 | 87 | assert settlement.preSignature(uid) > 0 88 | 89 | def test_swap_weth_for_aura_must_be_weth_aura(setup_aura_processor, weth, badger, usdc, aura, manager): 90 | ## Fail if opposite swap 91 | sell_amount = 100000000000000000000 92 | 93 | order_details = get_cowswap_order(setup_aura_processor, aura, weth, sell_amount) 94 | 95 | data = order_details.order_data 96 | uid = order_details.order_uid 97 | 98 | with brownie.reverts(): 99 | setup_aura_processor.swapWethForAURA(data, uid, {"from": manager}) 100 | 101 | ## Fail if selling non weth 102 | order_details = get_cowswap_order(setup_aura_processor, usdc, aura, sell_amount) 103 | 104 | data = order_details.order_data 105 | uid = order_details.order_uid 106 | 107 | with brownie.reverts(): 108 | setup_aura_processor.swapWethForAURA(data, uid, {"from": manager}) 109 | 110 | ## Fail if random token combo 111 | order_details = get_cowswap_order(setup_aura_processor, usdc, badger, sell_amount) 112 | 113 | data = order_details.order_data 114 | uid = order_details.order_uid 115 | 116 | with brownie.reverts(): 117 | setup_aura_processor.swapWethForAURA(data, uid, {"from": manager}) 118 | -------------------------------------------------------------------------------- /tests/on_chain_pricer/test_univ3_pricer_simu.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | from brownie import * 3 | import pytest 4 | 5 | """ 6 | sortUniV3Pools quote for stablecoin A swapped to stablecoin B which try for in-range swap before full-simulation 7 | https://info.uniswap.org/#/tokens/0x6b175474e89094c44da98b954eedeac495271d0f 8 | """ 9 | def test_simu_univ3_swap_sort_pools(oneE18, dai, usdc, weth, pricer): 10 | ## 1e18 11 | sell_amount = 10000 * oneE18 12 | 13 | ## minimum quote for DAI in USDC(1e6) 14 | p = 10000 * 0.999 * 1000000 15 | quoteInRangeAndFee = pricer.sortUniV3Pools(dai.address, sell_amount, usdc.address) 16 | 17 | ## min price 18 | assert quoteInRangeAndFee[0] >= p 19 | assert quoteInRangeAndFee[1] == 100 ## fee-0.01% pool got better quote than fee-0.05% pool 20 | 21 | def test_simu_univ3_swap_sort_pools_usdt(oneE18, usdt, weth, pricer): 22 | ## 1e18 23 | sell_amount = 10 * oneE18 24 | 25 | ## minimum quote for WETH in USDT(1e6) 26 | p = 10 * 600 * 1000000 27 | quoteInRangeAndFee = pricer.sortUniV3Pools(weth.address, sell_amount, usdt.address) 28 | 29 | ## min price 30 | assert quoteInRangeAndFee[0] >= p 31 | assert quoteInRangeAndFee[1] == 500 ## fee-0.05% pool 32 | 33 | quoteSETH2 = pricer.sortUniV3Pools(weth.address, sell_amount, "0xFe2e637202056d30016725477c5da089Ab0A043A") 34 | assert quoteSETH2[0] >= 10 * 0.999 * oneE18 35 | 36 | def test_simu_univ3_swap_usdt_usdc(oneE18, usdt, usdc, pricer): 37 | ## 1e18 38 | sell_amount = 10000 * 1000000 39 | 40 | ## minimum quote for USDC in USDT(1e6) 41 | p = 10000 * 0.999 * 1000000 42 | quoteInRangeAndFee = pricer.sortUniV3Pools(usdc.address, sell_amount, usdt.address) 43 | 44 | ## min price 45 | assert quoteInRangeAndFee[0] >= p 46 | assert quoteInRangeAndFee[1] == 100 ## fee-0.01% pool 47 | 48 | def test_simu_univ3_swap_tusd_usdc(oneE18, tusd, usdc, pricer): 49 | ## 1e18 50 | sell_amount = 10000 * 1000000 51 | 52 | ## minimum quote for USDC in TUSD(1e18) 53 | p = 10000 * 0.999 * oneE18 54 | quoteInRangeAndFee = pricer.sortUniV3Pools(usdc.address, sell_amount, tusd.address) 55 | 56 | ## min price 57 | assert quoteInRangeAndFee[0] >= p 58 | assert quoteInRangeAndFee[1] == 100 ## fee-0.01% pool 59 | 60 | quoteUSDM = pricer.sortUniV3Pools(usdc.address, sell_amount, "0xbbAec992fc2d637151dAF40451f160bF85f3C8C1") 61 | assert quoteUSDM[0] >= 10000 * 0.999 * 1000000 62 | 63 | def test_get_univ3_with_connector_no_second_pair(oneE18, balethbpt, usdc, weth, pricer): 64 | ## 1e18 65 | sell_amount = 10000 * 1000000 66 | 67 | ## no swap path for USDC -> WETH -> BALETHBPT in Uniswap V3 68 | quoteInRangeAndFee = pricer.getUniV3PriceWithConnector(usdc.address, sell_amount, balethbpt.address, weth.address) 69 | assert quoteInRangeAndFee == 0 70 | 71 | def test_get_univ3_with_connector_first_pair_quote_zero(oneE18, badger, usdc, weth, pricer): 72 | ## 1e18 73 | sell_amount = 10000 * 1000000 74 | 75 | ## not enough liquidity for path for BADGER -> WETH -> USDC in Uniswap V3 76 | quoteInRangeAndFee = pricer.getUniV3PriceWithConnector(badger.address, sell_amount, usdc.address, weth.address) 77 | assert quoteInRangeAndFee == 0 78 | 79 | def test_only_sushi_support(oneE18, xsushi, usdc, pricer): 80 | ## 1e18 81 | sell_amount = 100 * oneE18 82 | 83 | supported = pricer.isPairSupported(xsushi.address, usdc.address, sell_amount) 84 | assert supported == True 85 | 86 | def test_only_curve_support(oneE18, usdc, badger, aura, pricerwrapper): 87 | pricer = pricerwrapper 88 | ## 1e18 89 | sell_amount = 1000 * oneE18 90 | 91 | ## USDI 92 | supported = pricer.isPairSupported("0x2a54ba2964c8cd459dc568853f79813a60761b58", usdc.address, sell_amount) 93 | assert supported == True 94 | quoteTx = pricer.findOptimalSwap("0x2a54ba2964c8cd459dc568853f79813a60761b58", usdc.address, sell_amount) 95 | assert quoteTx[1][1] > 0 96 | assert quoteTx[1][0] == 0 97 | 98 | ## not supported yet 99 | isBadgerAuraSupported = pricer.isPairSupported(badger.address, aura.address, sell_amount * 100) 100 | assert isBadgerAuraSupported == False 101 | -------------------------------------------------------------------------------- /contracts/libraries/balancer/BalancerFixedPoint.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.7.6; 3 | pragma abicoder v2; 4 | 5 | import "./BalancerLogExpMath.sol"; 6 | 7 | // https://github.com/balancer-labs/balancer-v2-monorepo/blob/master/pkg/solidity-utils/contracts/math/FixedPoint.sol 8 | library BalancerFixedPoint { 9 | uint256 internal constant ONE = 1e18; // 18 decimal places 10 | uint256 internal constant TWO = 2 * ONE; 11 | uint256 internal constant FOUR = 4 * ONE; 12 | uint256 internal constant MAX_POW_RELATIVE_ERROR = 10000; // 10^(-14) 13 | 14 | function add(uint256 a, uint256 b) internal pure returns (uint256) { 15 | uint256 c = a + b; 16 | require(c >= a, '!add'); 17 | return c; 18 | } 19 | 20 | function sub(uint256 a, uint256 b) internal pure returns (uint256) { 21 | require(b <= a, '!sub'); 22 | uint256 c = a - b; 23 | return c; 24 | } 25 | 26 | function divUp(uint256 a, uint256 b) internal pure returns (uint256) { 27 | require(b != 0, '!b0'); 28 | 29 | if (a == 0) { 30 | return 0; 31 | } else { 32 | uint256 aInflated = a * ONE; 33 | require(aInflated / a == ONE, '!divU'); // mul overflow 34 | 35 | // The traditional divUp formula is: 36 | // divUp(x, y) := (x + y - 1) / y 37 | // To avoid intermediate overflow in the addition, we distribute the division and get: 38 | // divUp(x, y) := (x - 1) / y + 1 39 | // Note that this requires x != 0, which we already tested for. 40 | 41 | return ((aInflated - 1) / b) + 1; 42 | } 43 | } 44 | 45 | function divDown(uint256 a, uint256 b) internal pure returns (uint256) { 46 | require(b != 0, '!b0'); 47 | 48 | if (a == 0) { 49 | return 0; 50 | } else { 51 | uint256 aInflated = a * ONE; 52 | require(aInflated / a == ONE, 'divD'); // mul overflow 53 | 54 | return aInflated / b; 55 | } 56 | } 57 | 58 | function mulUp(uint256 a, uint256 b) internal pure returns (uint256) { 59 | uint256 product = a * b; 60 | require(a == 0 || product / a == b, '!mul'); 61 | 62 | if (product == 0) { 63 | return 0; 64 | } else { 65 | // The traditional divUp formula is: 66 | // divUp(x, y) := (x + y - 1) / y 67 | // To avoid intermediate overflow in the addition, we distribute the division and get: 68 | // divUp(x, y) := (x - 1) / y + 1 69 | // Note that this requires x != 0, which we already tested for. 70 | 71 | return ((product - 1) / ONE) + 1; 72 | } 73 | } 74 | 75 | /** 76 | * @dev Returns x^y, assuming both are fixed point numbers, rounding up. The result is guaranteed to not be below 77 | * the true value (that is, the error function expected - actual is always negative). 78 | */ 79 | function powUp(uint256 x, uint256 y) internal pure returns (uint256) { 80 | // 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 81 | // and 80/20 Weighted Pools 82 | if (y == ONE) { 83 | return x; 84 | } else if (y == TWO) { 85 | return mulUp(x, x); 86 | } else if (y == FOUR) { 87 | uint256 square = mulUp(x, x); 88 | return mulUp(square, square); 89 | } else { 90 | uint256 raw = BalancerLogExpMath.pow(x, y); 91 | uint256 maxError = add(mulUp(raw, MAX_POW_RELATIVE_ERROR), 1); 92 | 93 | return add(raw, maxError); 94 | } 95 | } 96 | 97 | function mulDown(uint256 a, uint256 b) internal pure returns (uint256) { 98 | uint256 product = a * b; 99 | require(a == 0 || product / a == b, 'mulD'); 100 | 101 | return product / ONE; 102 | } 103 | 104 | /** 105 | * @dev Returns the complement of a value (1 - x), capped to 0 if x is larger than 1. 106 | * 107 | * Useful when computing the complement for values with some level of relative error, as it strips this error and 108 | * prevents intermediate negative values. 109 | */ 110 | function complement(uint256 x) internal pure returns (uint256) { 111 | return (x < ONE) ? (ONE - x) : 0; 112 | } 113 | } -------------------------------------------------------------------------------- /tests/aura_processor/test_real_life.py: -------------------------------------------------------------------------------- 1 | from time import sleep 2 | import brownie 3 | from brownie import * 4 | import pytest 5 | from scripts.send_order import get_cowswap_order 6 | 7 | LIVE_PROCESSOR = "0x8abd28e4d69bd3953b96dd9ed63533765adb9965" 8 | 9 | EXPECT_REVERT = True 10 | 11 | """ 12 | NOTE: 13 | Neutrino 14 | T 15 | Worhmhole UST 16 | All fail at the Cowswap level 17 | TODO: Ack / Remove or leave as failing 18 | """ 19 | 20 | @pytest.fixture 21 | def reverting_contract(): 22 | return AuraBribesProcessor.at(LIVE_PROCESSOR) 23 | 24 | 25 | 26 | def test_does_it_revert_with_v2_pricer(reverting_contract, weth, usdc, usdc_whale, lenient_contract): 27 | """ 28 | Basic revert check to proove that V2 Pricer breaks V3 Processor 29 | """ 30 | sell_amount = 123000000 31 | 32 | order_details = get_cowswap_order(reverting_contract, usdc, weth, sell_amount) 33 | 34 | usdc.transfer(reverting_contract, sell_amount, {"from": usdc_whale}) 35 | 36 | 37 | data = order_details.order_data 38 | uid = order_details.order_uid 39 | 40 | real_manager = accounts.at(reverting_contract.manager(), force=True) 41 | 42 | ## Change this after governance has fixed 43 | if EXPECT_REVERT: 44 | with brownie.reverts(): 45 | reverting_contract.sellBribeForWeth(data, uid, {"from": real_manager}) 46 | 47 | 48 | ## Deploy new pricer 49 | new_pricer = lenient_contract 50 | 51 | dev_multi = accounts.at(reverting_contract.DEV_MULTI(), force=True) 52 | 53 | reverting_contract.setPricer(new_pricer, {"from": dev_multi}) 54 | 55 | reverting_contract.sellBribeForWeth(data, uid, {"from": real_manager}) 56 | 57 | 58 | BRIBES_TOKEN_CLAIMABLE = [ 59 | ("0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B", 18), ## CVX 60 | ("0x6B175474E89094C44Da98b954EedeAC495271d0F", 18), ## DAI 61 | ("0x090185f2135308bad17527004364ebcc2d37e5f6", 22), ## SPELL ## NOTE: Using 22 to adjust as spell is super high supply 62 | ("0xdbdb4d16eda451d0503b854cf79d55697f90c8df", 18), ## ALCX 63 | ("0x9D79d5B61De59D882ce90125b18F74af650acB93", 8), ## NSBT ## NOTE: Using 6 + 2 decimals to make it more 64 | ("0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0", 18), ## MATIC 65 | ("0x3432b6a60d23ca0dfca7761b7ab56459d9c964d0", 18), ## FXS 66 | ("0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32", 18), ## LDO 67 | ("0xc7283b66Eb1EB5FB86327f08e1B5816b0720212B", 18), ## TRIBE 68 | ("0x8207c1FfC5B6804F6024322CcF34F29c3541Ae26", 18), ## OGN 69 | ("0xa3BeD4E1c75D00fa6f4E5E6922DB7261B5E9AcD2", 18), ## MTA 70 | ("0x31429d1856aD1377A8A0079410B297e1a9e214c2", 22), ## ANGLE ## NOTE Using 18 + 4 to raise the value 71 | ("0xCdF7028ceAB81fA0C6971208e83fa7872994beE5", 22), ## T ## NOTE Using 18 + 4 to raise the value 72 | ("0xa693B19d2931d498c5B318dF961919BB4aee87a5", 6), # UST 73 | ("0xB620Be8a1949AA9532e6a3510132864EF9Bc3F82", 22), ## LFT ## NOTE Using 18 + 4 to raise the value 74 | ("0x6243d8CEA23066d098a15582d81a598b4e8391F4", 18), ## FLX 75 | ("0x3Ec8798B81485A254928B70CDA1cf0A2BB0B74D7", 18), ## GRO 76 | ("0xAf5191B0De278C7286d6C7CC6ab6BB8A73bA2Cd6", 18), ## STG 77 | ("0xdB25f211AB05b1c97D595516F45794528a807ad8", 2), ## EURS 78 | ("0x674C6Ad92Fd080e4004b2312b45f796a192D27a0", 18), ## USDN 79 | ("0xFEEf77d3f69374f66429C91d732A244f074bdf74", 18), ## cvxFXS 80 | ("0x41D5D79431A913C4aE7d69a668ecdfE5fF9DFB68", 18)## INV 81 | ] 82 | 83 | 84 | @pytest.fixture 85 | def fixed_contract(lenient_contract, reverting_contract): 86 | new_pricer = lenient_contract 87 | 88 | dev_multi = accounts.at(reverting_contract.DEV_MULTI(), force=True) 89 | 90 | reverting_contract.setPricer(new_pricer, {"from": dev_multi}) 91 | 92 | return reverting_contract 93 | 94 | 95 | @pytest.mark.parametrize("token, decimals", BRIBES_TOKEN_CLAIMABLE) 96 | def test_fixed_with_v3_pricer_token_check(fixed_contract, token, decimals, weth): 97 | """ 98 | Prove that V3 Processor with V3 Pricer works for all the bribes mentioned above 99 | """ 100 | 101 | real_manager = accounts.at(fixed_contract.manager(), force=True) 102 | 103 | sell_amount = 1000 * 10 ** decimals 104 | 105 | token = interface.ERC20(token) 106 | 107 | sleep(1) ## Wait 1 second to avoid getting timed out 108 | 109 | order_details = get_cowswap_order(fixed_contract, token, weth, sell_amount) 110 | data = order_details.order_data 111 | uid = order_details.order_uid 112 | 113 | fixed_contract.sellBribeForWeth(data, uid, {"from": real_manager}) 114 | 115 | 116 | -------------------------------------------------------------------------------- /scripts/send_order_rinkeby.py: -------------------------------------------------------------------------------- 1 | from time import time 2 | import requests 3 | from brownie import * 4 | import click 5 | from rich.console import Console 6 | from dotmap import DotMap 7 | 8 | console = Console() 9 | 10 | WETH = "0xc778417E063141139Fce010982780140Aa0cD5Ab" 11 | DAI = "0x5592EC0cfb4dbc12D3aB100b257153436a1f0FEa" 12 | PROCESSOR = "0x073611461A526bc7CC90C6d16927C8032BbA8D3C" 13 | 14 | def main(): 15 | processor = TestProcessor.at(PROCESSOR) 16 | 17 | dai = interface.ERC20(DAI) 18 | weth = interface.ERC20(WETH) 19 | 20 | amount = weth.balanceOf(processor) 21 | 22 | cowswap_sell_demo(processor, weth, dai, amount) 23 | 24 | def cowswap_sell_demo(contract, sell_token, buy_token, amount_in): 25 | """ 26 | Demo of placing order and verifying it 27 | """ 28 | amount = amount_in 29 | 30 | # get the fee + the buy amount after fee 31 | ## TODO: Refactor to new, better endpoint: https://discord.com/channels/869166959739170836/935460632818516048/953702376345309254 32 | fee_and_quote = "https://api.cow.fi/rinkeby/api/v1/feeAndQuote/sell" 33 | get_params = { 34 | "sellToken": sell_token.address, 35 | "buyToken": buy_token.address, 36 | "sellAmountBeforeFee": amount 37 | } 38 | r = requests.get(fee_and_quote, params=get_params) 39 | assert r.ok and r.status_code == 200 40 | 41 | # These two values are needed to create an order 42 | fee_amount = int(r.json()['fee']['amount']) 43 | buy_amount_after_fee = int(r.json()['buyAmountAfterFee']) 44 | assert fee_amount > 0 45 | assert buy_amount_after_fee > 0 46 | 47 | # Pretty random order deadline :shrug: 48 | deadline = chain.time() + 60*60*1 # 1 hour 49 | 50 | # Submit order 51 | order_payload = { 52 | "sellToken": sell_token.address, 53 | "buyToken": buy_token.address, 54 | "sellAmount": str(amount-fee_amount), # amount that we have minus the fee we have to pay 55 | "buyAmount": str(buy_amount_after_fee), # buy amount fetched from the previous call 56 | "validTo": deadline, 57 | "appData": "0x2B8694ED30082129598720860E8E972F07AA10D9B81CAE16CA0E2CFB24743E24", # maps to https://bafybeiblq2ko2maieeuvtbzaqyhi5fzpa6vbbwnydsxbnsqoft5si5b6eq.ipfs.dweb.link 58 | "feeAmount": str(fee_amount), 59 | "kind": "sell", 60 | "partiallyFillable": False, 61 | "receiver": contract.address, 62 | "signature": contract.address, 63 | "from": contract.address, 64 | "sellTokenBalance": "erc20", 65 | "buyTokenBalance": "erc20", 66 | "signingScheme": "presign" # Very important. this tells the api you are going to sign on chain 67 | } 68 | orders_url = f"https://api.cow.fi/rinkeby/api/v1/orders" 69 | r = requests.post(orders_url, json=order_payload) 70 | assert r.ok and r.status_code == 201 71 | order_uid = r.json() 72 | print(f"Payload: {order_payload}") 73 | print(f"Order uid: {order_uid}") 74 | 75 | # IERC20 sellToken; 76 | # IERC20 buyToken; 77 | # address receiver; 78 | # uint256 sellAmount; 79 | # uint256 buyAmount; 80 | # uint32 validTo; 81 | # bytes32 appData; 82 | # uint256 feeAmount; 83 | # bytes32 kind; 84 | # bool partiallyFillable; 85 | # bytes32 sellTokenBalance; 86 | # bytes32 buyTokenBalance; 87 | order_data = [ 88 | sell_token.address, 89 | buy_token.address, 90 | contract.address, 91 | amount-fee_amount, 92 | buy_amount_after_fee, 93 | deadline, 94 | "0x2B8694ED30082129598720860E8E972F07AA10D9B81CAE16CA0E2CFB24743E24", 95 | fee_amount, 96 | contract.KIND_SELL(), 97 | False, 98 | contract.BALANCE_ERC20(), 99 | contract.BALANCE_ERC20() 100 | ] 101 | 102 | hashFromContract = contract.getHash(order_data, contract.domainSeparator()) 103 | print(f"Hash from Contract: {hashFromContract}") 104 | fromContract = contract.getOrderID(order_data) 105 | print(f"Order uid from Contract: {fromContract}") 106 | 107 | contract.checkCowswapOrder(order_data, order_uid) 108 | 109 | dev = connect_account() 110 | 111 | contract.doCowswapOrder(order_data, order_uid, {"from": dev}) 112 | return order_uid 113 | 114 | ## TODO Refactor to return hash map with all the fields 115 | 116 | 117 | def connect_account(): 118 | click.echo(f"You are using the '{network.show_active()}' network") 119 | dev = accounts.load(click.prompt("Account", type=click.Choice(accounts.load()))) 120 | click.echo(f"You are using: 'dev' [{dev.address}]") 121 | return dev 122 | -------------------------------------------------------------------------------- /tests/aura_processor/test_emit_aura.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | from brownie import a 3 | from helpers.utils import approx 4 | 5 | """ 6 | swapAuraToBveAuraAndEmit 7 | Works for both LP and Buy 8 | Emits event 9 | 10 | emitBadger 11 | Works 12 | Emits event 13 | """ 14 | 15 | OPS_FEE = 0.05 # Hardcoded in contract 16 | 17 | def test_swap_aura_and_emit(setup_aura_processor, manager, aura, bve_aura): 18 | bve_tree_balance_before = bve_aura.balanceOf(setup_aura_processor.BADGER_TREE()) 19 | bve_processor_balance_before = bve_aura.balanceOf(setup_aura_processor.address) # There could be gravi beforehand 20 | bve_treasury_balance_before = bve_aura.balanceOf(setup_aura_processor.TREASURY()) 21 | assert aura.balanceOf(setup_aura_processor) > 0 22 | 23 | bve_supply = bve_aura.totalSupply() 24 | 25 | # Only manager can call 26 | with brownie.reverts(): 27 | setup_aura_processor.swapAURATobveAURAAndEmit({"from": a[9]}) 28 | 29 | tx = setup_aura_processor.swapAURATobveAURAAndEmit({"from": manager}) 30 | 31 | assert bve_aura.balanceOf(setup_aura_processor.BADGER_TREE()) > bve_tree_balance_before 32 | assert aura.balanceOf(setup_aura_processor.address) == 0 ## All aura has been emitted 33 | 34 | ## graviAURA supply increased due to deposit 35 | graviaura_acquird = bve_aura.totalSupply() - bve_supply 36 | graviaura_total = graviaura_acquird + bve_processor_balance_before 37 | assert graviaura_acquird > 0 38 | 39 | # Confirm math 40 | ops_fee = int(graviaura_total) * OPS_FEE 41 | # 1% approximation due to Brownie rounding 42 | assert approx(ops_fee, bve_aura.balanceOf(setup_aura_processor.TREASURY()) - bve_treasury_balance_before, 1) 43 | 44 | to_emit = graviaura_total - ops_fee 45 | 46 | # Confirm events 47 | # Tree Distribution 48 | assert len(tx.events["TreeDistribution"]) == 1 49 | event = tx.events["TreeDistribution"][0] 50 | assert event["token"] == bve_aura.address 51 | assert approx(event["amount"], to_emit, 1) 52 | assert event["beneficiary"] == bve_aura.address 53 | 54 | # Performance Fee Governance 55 | assert len(tx.events["PerformanceFeeGovernance"]) == 1 56 | event = tx.events["PerformanceFeeGovernance"][0] 57 | assert event["token"] == bve_aura.address 58 | assert approx(event["amount"], ops_fee, 1) 59 | 60 | # BribesEmission 61 | assert len(tx.events["BribeEmission"]) == 1 62 | event = tx.events["BribeEmission"][0] 63 | assert event["token"] == bve_aura.address 64 | assert approx(event["amount"], to_emit, 1) 65 | 66 | ## Reverts if called a second time 67 | with brownie.reverts(): 68 | setup_aura_processor.swapAURATobveAURAAndEmit({"from": manager}) 69 | 70 | 71 | def test_emit_badger(setup_aura_processor, manager, badger, bve_aura): 72 | badger_tree_balance_before = badger.balanceOf(setup_aura_processor.BADGER_TREE()) 73 | badger_processor_balance_before = badger.balanceOf(setup_aura_processor) 74 | badger_treasury_balance_before = badger.balanceOf(setup_aura_processor.TREASURY()) 75 | assert badger_processor_balance_before > 0 76 | 77 | # Only manager can call 78 | with brownie.reverts(): 79 | setup_aura_processor.emitBadger({"from": a[9]}) 80 | 81 | tx = setup_aura_processor.emitBadger({"from": manager}) 82 | 83 | assert badger.balanceOf(setup_aura_processor.BADGER_TREE()) > badger_tree_balance_before 84 | assert badger.balanceOf(setup_aura_processor) == 0 ## All badger emitted 85 | 86 | # Confirm math 87 | ops_fee = int(badger_processor_balance_before) * OPS_FEE 88 | # 1% approximation due to Brownie rounding 89 | assert approx(ops_fee, badger.balanceOf(setup_aura_processor.TREASURY()) - badger_treasury_balance_before, 1) 90 | 91 | to_emit = badger_processor_balance_before - ops_fee 92 | 93 | # Confirm events 94 | # Tree Distribution 95 | assert len(tx.events["TreeDistribution"]) == 1 96 | event = tx.events["TreeDistribution"][0] 97 | assert event["token"] == badger.address 98 | assert approx(event["amount"], to_emit, 1) 99 | assert event["beneficiary"] == bve_aura.address 100 | 101 | # Performance Fee Governance 102 | assert len(tx.events["PerformanceFeeGovernance"]) == 1 103 | event = tx.events["PerformanceFeeGovernance"][0] 104 | assert event["token"] == badger.address 105 | assert approx(event["amount"], ops_fee, 1) 106 | 107 | # BribesEmission 108 | assert len(tx.events["BribeEmission"]) == 1 109 | event = tx.events["BribeEmission"][0] 110 | assert event["token"] == badger.address 111 | assert approx(event["amount"], to_emit, 1) 112 | 113 | ## Reverts if called a second time 114 | with brownie.reverts(): 115 | setup_aura_processor.emitBadger({"from": manager}) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fair Selling 2 | 3 | A [BadgerDAO](https://app.badger.com/) sponsored repo of Open Source Contracts for: 4 | - Integrating Smart Contracts with Cowswap 5 | - Non-Custodial handling of tokens via BribesProcessors 6 | - Calculating onChain Prices 7 | - Executing the best onChain Swap 8 | 9 | ## Why bother 10 | 11 | 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. 12 | 13 | However we believe that given a set of constraints (available Dexes, handpicked), we can efficiently compute the best trade available to us 14 | 15 | In exploring this issue we aim to: 16 | - Find the most gas-efficient way to get the best executable price (currently 120 /150k per quote, from 1.6MLN) 17 | - Finding the most reliable price we can, to determine if an offer is fair or unfair (Cowswap integration) 18 | - Can we create a "trustless swap" that is provably not frontrun nor manipulated? 19 | - 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) 20 | 21 | ## Current Release V0.3 - Pricer - BribesProcessor - CowswapSeller 22 | 23 | # Notable Contracts 24 | ## CowswapSeller 25 | 26 | OnChain Integration with Cowswap, all the functions you want to: 27 | - Verify an Order 28 | - Retrieve the OrderUid from the orderData 29 | - Validate an order through basic security checks (price is correct, sends to correct recipient) 30 | - Integrated with an onChain Pricer (see below), to offer stronger execution guarantees 31 | 32 | ## BribesProcessor 33 | 34 | Anti-rug technplogy, allows a Multi-sig to rapidly process CowSwap orders, without allowing the Multi to rug 35 | Allows tokens to be rescued without the need for governance via the `ragequit` function 36 | 37 | - `AuraBribesProcessor` -> Processor for Votium Bribes earned by `bveAura` 38 | - `VotiumBribesProcessor` -> Processor for Votium Bribes earned by `bveCVX` 39 | 40 | ## OnChainPricingMainnet 41 | 42 | Given a tokenIn, tokenOut and AmountIn, returns a Quote from the most popular dexes 43 | 44 | - `OnChainPricingMainnet` -> Fully onChain math to find best, single source swap (no fragmented swaps yet) 45 | - `OnChainPricingMainnetLenient` -> Slippage tollerant version of the Pricer 46 | 47 | ### Dexes Support 48 | - Curve 49 | - UniV2 50 | - UniV3 51 | - Balancer 52 | - Sushi 53 | 54 | Covering >80% TVL on Mainnet. (Prob even more) 55 | 56 | ## Example Usage 57 | 58 | BREAKING CHANGE: V3 is back to `view` even for Balancer and UniV3 functions 59 | 60 | ### isPairSupported 61 | 62 | Returns true if the pricer will return a non-zero quote 63 | NOTE: This is not proof of optimality 64 | 65 | ```solidity 66 | /// @dev Given tokenIn, out and amountIn, returns true if a quote will be non-zero 67 | /// @notice Doesn't guarantee optimality, just non-zero 68 | function isPairSupported(address tokenIn, address tokenOut, uint256 amountIn) external returns (bool) 69 | ``` 70 | 71 | In Brownie 72 | ```python 73 | quote = pricer.isPairSupported(t_in, t_out, amt_in) 74 | ``` 75 | 76 | ### findOptimalSwap 77 | 78 | Returns the best quote given the various Dexes, used Heuristics to save gas (V0.3 will focus on this) 79 | NOTE: While the function says optimal, this is not optimal, just best of the bunch, optimality may never be achieved fully on-chain 80 | 81 | ```solidity 82 | function findOptimalSwap(address tokenIn, address tokenOut, uint256 amountIn) external virtual returns (Quote memory) 83 | ``` 84 | 85 | In Brownie 86 | ```python 87 | quote = pricer.findOptimalSwap(t_in, t_out, amt_in) 88 | ``` 89 | 90 | 91 | # Mainnet Pricing Lenient 92 | 93 | Variation of Pricer with a slippage tollerance 94 | 95 | 96 | 97 | # Notable Tests 98 | 99 | ## Proof that the math is accurate with gas savings 100 | 101 | These tests compare the PricerV3 (150k per quote) against V2 (1.6MLN per quote) 102 | 103 | ``` 104 | brownie test tests/heuristic_equivalency/test_heuristic_equivalency.py 105 | 106 | ``` 107 | 108 | ## Benchmark specific AMM quotes 109 | TODO: Improve to just use the specific quote 110 | 111 | ``` 112 | brownie test tests/gas_benchmark/benchmark_pricer_gas.py --gas 113 | ``` 114 | 115 | ## Benchmark coverage of top DeFi Tokens 116 | 117 | TODO: Add like 200 tokens 118 | TODO: Compare against Coingecko API or smth 119 | 120 | ``` 121 | brownie test tests/gas_benchmark/benchmark_token_coverage.py --gas 122 | ``` 123 | 124 | ## Notable Test from V2 125 | 126 | Run V3 Pricer against V2, to confirm results are correct, but with gas savings 127 | 128 | ``` 129 | brownie test tests/heuristic_equivalency/test_heuristic_equivalency.py 130 | ``` 131 | 132 | 133 | # Deployments 134 | 135 | WARNING: This list is not maintained and may be out of date or incorrect. DYOR. 136 | 137 | `bveCVX Bribes Processor`: https://etherscan.io/address/0xb2bf1d48f2c2132913278672e6924efda3385de2 138 | 139 | `bveAURA Bribes Processor`: https://etherscan.io/address/0x0b6198b324e12a002b60162f8a130d6aedabd04c 140 | 141 | Pricers can be found by checking `processor.pricer()` -------------------------------------------------------------------------------- /contracts/libraries/uniswap/FullMath.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.7.6; 3 | pragma abicoder v2; 4 | 5 | // https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/FullMath.sol 6 | library FullMath { 7 | /// @notice Calculates floor(a×b÷denominator) with full precision. Throws if result overflows a uint256 or denominator == 0 8 | /// @param a The multiplicand 9 | /// @param b The multiplier 10 | /// @param denominator The divisor 11 | /// @return result The 256-bit result 12 | /// @dev Credit to Remco Bloemen under MIT license https://xn--2-umb.com/21/muldiv 13 | function mulDiv( 14 | uint256 a, 15 | uint256 b, 16 | uint256 denominator 17 | ) internal pure returns (uint256 result) { 18 | // 512-bit multiply [prod1 prod0] = a * b 19 | // Compute the product mod 2**256 and mod 2**256 - 1 20 | // then use the Chinese Remainder Theorem to reconstruct 21 | // the 512 bit result. The result is stored in two 256 22 | // variables such that product = prod1 * 2**256 + prod0 23 | uint256 prod0; // Least significant 256 bits of the product 24 | uint256 prod1; // Most significant 256 bits of the product 25 | assembly { 26 | let mm := mulmod(a, b, not(0)) 27 | prod0 := mul(a, b) 28 | prod1 := sub(sub(mm, prod0), lt(mm, prod0)) 29 | } 30 | 31 | // Handle non-overflow cases, 256 by 256 division 32 | if (prod1 == 0) { 33 | require(denominator > 0); 34 | assembly { 35 | result := div(prod0, denominator) 36 | } 37 | return result; 38 | } 39 | 40 | // Make sure the result is less than 2**256. 41 | // Also prevents denominator == 0 42 | require(denominator > prod1); 43 | 44 | /////////////////////////////////////////////// 45 | // 512 by 256 division. 46 | /////////////////////////////////////////////// 47 | 48 | // Make division exact by subtracting the remainder from [prod1 prod0] 49 | // Compute remainder using mulmod 50 | uint256 remainder; 51 | assembly { 52 | remainder := mulmod(a, b, denominator) 53 | } 54 | // Subtract 256 bit number from 512 bit number 55 | assembly { 56 | prod1 := sub(prod1, gt(remainder, prod0)) 57 | prod0 := sub(prod0, remainder) 58 | } 59 | 60 | // Factor powers of two out of denominator 61 | // Compute largest power of two divisor of denominator. 62 | // Always >= 1. 63 | uint256 twos = -denominator & denominator; 64 | // Divide denominator by power of two 65 | assembly { 66 | denominator := div(denominator, twos) 67 | } 68 | 69 | // Divide [prod1 prod0] by the factors of two 70 | assembly { 71 | prod0 := div(prod0, twos) 72 | } 73 | // Shift in bits from prod1 into prod0. For this we need 74 | // to flip `twos` such that it is 2**256 / twos. 75 | // If twos is zero, then it becomes one 76 | assembly { 77 | twos := add(div(sub(0, twos), twos), 1) 78 | } 79 | prod0 |= prod1 * twos; 80 | 81 | // Invert denominator mod 2**256 82 | // Now that denominator is an odd number, it has an inverse 83 | // modulo 2**256 such that denominator * inv = 1 mod 2**256. 84 | // Compute the inverse by starting with a seed that is correct 85 | // correct for four bits. That is, denominator * inv = 1 mod 2**4 86 | uint256 inv = (3 * denominator) ^ 2; 87 | // Now use Newton-Raphson iteration to improve the precision. 88 | // Thanks to Hensel's lifting lemma, this also works in modular 89 | // arithmetic, doubling the correct bits in each step. 90 | inv *= 2 - denominator * inv; // inverse mod 2**8 91 | inv *= 2 - denominator * inv; // inverse mod 2**16 92 | inv *= 2 - denominator * inv; // inverse mod 2**32 93 | inv *= 2 - denominator * inv; // inverse mod 2**64 94 | inv *= 2 - denominator * inv; // inverse mod 2**128 95 | inv *= 2 - denominator * inv; // inverse mod 2**256 96 | 97 | // Because the division is now exact we can divide by multiplying 98 | // with the modular inverse of denominator. This will give us the 99 | // correct result modulo 2**256. Since the precoditions guarantee 100 | // that the outcome is less than 2**256, this is the final result. 101 | // We don't need to compute the high bits of the result and prod1 102 | // is no longer required. 103 | result = prod0 * inv; 104 | return result; 105 | } 106 | 107 | /// @notice Calculates ceil(a×b÷denominator) with full precision. Throws if result overflows a uint256 or denominator == 0 108 | /// @param a The multiplicand 109 | /// @param b The multiplier 110 | /// @param denominator The divisor 111 | /// @return result The 256-bit result 112 | function mulDivRoundingUp( 113 | uint256 a, 114 | uint256 b, 115 | uint256 denominator 116 | ) internal pure returns (uint256 result) { 117 | result = mulDiv(a, b, denominator); 118 | if (mulmod(a, b, denominator) > 0) { 119 | require(result < type(uint256).max); 120 | result++; 121 | } 122 | } 123 | } -------------------------------------------------------------------------------- /tests/on_chain_pricer/test_swap_exec_on_chain.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | from brownie import * 3 | 4 | import pytest 5 | 6 | """ 7 | test swap in Curve from token A to token B directly 8 | """ 9 | def test_swap_in_curve(oneE18, weth_whale, weth, crv, pricer, swapexecutor): 10 | ## 1e18 11 | sell_amount = 1 * oneE18 12 | 13 | ## minimum quote for ETH in CRV 14 | p = 1 * 1000 * oneE18 15 | pool = '0x8e764bE4288B842791989DB5b8ec067279829809' 16 | quote = pricer.getCurvePrice(pool, weth.address, crv.address, sell_amount) 17 | assert quote[1] >= p 18 | 19 | ## swap on chain 20 | slippageTolerance = 0.95 21 | weth.transfer(swapexecutor.address, sell_amount, {'from': weth_whale}) 22 | 23 | minOutput = quote[1] * slippageTolerance 24 | balBefore = crv.balanceOf(weth_whale) 25 | poolBytes = pricer.convertToBytes32(quote[0]) 26 | swapexecutor.doOptimalSwapWithQuote(weth.address, crv.address, sell_amount, (0, minOutput, [poolBytes], []), {'from': weth_whale}) 27 | balAfter = crv.balanceOf(weth_whale) 28 | assert (balAfter - balBefore) >= minOutput 29 | 30 | """ 31 | test swap in Uniswap V2 from token A to token B directly 32 | """ 33 | def test_swap_in_univ2(oneE18, weth_whale, weth, usdc, pricer, swapexecutor): 34 | ## 1e18 35 | sell_amount = 1 * oneE18 36 | uniV2Router = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D' 37 | 38 | ## minimum quote for ETH in USDC(1e6) 39 | p = 1 * 500 * 1000000 40 | quote = pricer.getUniPrice(uniV2Router, weth.address, usdc.address, sell_amount) 41 | assert quote >= p 42 | 43 | ## swap on chain 44 | slippageTolerance = 0.95 45 | weth.transfer(swapexecutor.address, sell_amount, {'from': weth_whale}) 46 | 47 | minOutput = quote * slippageTolerance 48 | balBefore = usdc.balanceOf(weth_whale) 49 | swapexecutor.doOptimalSwapWithQuote(weth.address, usdc.address, sell_amount, (1, minOutput, [], []), {'from': weth_whale}) 50 | balAfter = usdc.balanceOf(weth_whale) 51 | assert (balAfter - balBefore) >= minOutput 52 | 53 | """ 54 | test swap in Uniswap V3 from token A to token B directly 55 | """ 56 | def test_swap_in_univ3_single(oneE18, wbtc_whale, wbtc, usdc, pricer, swapexecutor): 57 | ## 1e8 58 | sell_amount = 1 * 100000000 59 | 60 | ## minimum quote for WBTC in USDC(1e6) 61 | p = 1 * 15000 * 1000000 62 | 63 | ## swap on chain 64 | slippageTolerance = 0.95 65 | wbtc.transfer(swapexecutor.address, sell_amount, {'from': wbtc_whale}) 66 | 67 | minOutput = p * slippageTolerance 68 | balBefore = usdc.balanceOf(wbtc_whale) 69 | swapexecutor.doOptimalSwapWithQuote(wbtc.address, usdc.address, sell_amount, (3, minOutput, [], [3000]), {'from': wbtc_whale}) 70 | balAfter = usdc.balanceOf(wbtc_whale) 71 | assert (balAfter - balBefore) >= minOutput 72 | 73 | """ 74 | test swap in Uniswap V3 from token A to token B via connectorToken C 75 | """ 76 | 77 | def test_swap_in_univ3(oneE18, wbtc_whale, wbtc, weth, usdc, pricer, swapexecutor): 78 | ## 1e8 79 | sell_amount = 1 * 100000000 80 | 81 | ## minimum quote for WBTC in USDC(1e6) 82 | p = 1 * 15000 * 1000000 83 | 84 | ## swap on chain 85 | slippageTolerance = 0.95 86 | wbtc.transfer(swapexecutor.address, sell_amount, {'from': wbtc_whale}) 87 | 88 | minOutput = p * slippageTolerance 89 | ## encodedPath = swapexecutor.encodeUniV3TwoHop(wbtc.address, 500, weth.address, 500, usdc.address) 90 | balBefore = usdc.balanceOf(wbtc_whale) 91 | swapexecutor.doOptimalSwapWithQuote(wbtc.address, usdc.address, sell_amount, (4, minOutput, [], [500,500]), {'from': wbtc_whale}) 92 | balAfter = usdc.balanceOf(wbtc_whale) 93 | assert (balAfter - balBefore) >= minOutput 94 | 95 | """ 96 | test swap in Balancer V2 from token A to token B via connectorToken C 97 | """ 98 | def test_swap_in_balancer_batch(oneE18, wbtc_whale, wbtc, weth, usdc, pricer, swapexecutor): 99 | ## 1e8 100 | sell_amount = 1 * 100000000 101 | 102 | ## minimum quote for WBTC in USDC(1e6) 103 | p = 1 * 15000 * 1000000 104 | 105 | ## swap on chain 106 | slippageTolerance = 0.95 107 | wbtc.transfer(swapexecutor.address, sell_amount, {'from': wbtc_whale}) 108 | 109 | minOutput = p * slippageTolerance 110 | wbtc2WETHPoolId = '0xa6f548df93de924d73be7d25dc02554c6bd66db500020000000000000000000e' 111 | weth2USDCPoolId = '0x96646936b91d6b9d7d0c47c496afbf3d6ec7b6f8000200000000000000000019' 112 | balBefore = usdc.balanceOf(wbtc_whale) 113 | swapexecutor.doOptimalSwapWithQuote(wbtc.address, usdc.address, sell_amount, (6, minOutput, [wbtc2WETHPoolId,weth2USDCPoolId], []), {'from': wbtc_whale}) 114 | balAfter = usdc.balanceOf(wbtc_whale) 115 | assert (balAfter - balBefore) >= minOutput 116 | 117 | """ 118 | test swap in Balancer V2 from token A to token B directly 119 | """ 120 | def test_swap_in_balancer_single(oneE18, weth_whale, weth, usdc, pricer, swapexecutor): 121 | ## 1e18 122 | sell_amount = 1 * oneE18 123 | 124 | ## minimum quote for WETH in USDC(1e6) 125 | p = 1 * 500 * 1000000 126 | 127 | ## swap on chain 128 | slippageTolerance = 0.95 129 | weth.transfer(swapexecutor.address, sell_amount, {'from': weth_whale}) 130 | 131 | minOutput = p * slippageTolerance 132 | weth2USDCPoolId = '0x96646936b91d6b9d7d0c47c496afbf3d6ec7b6f8000200000000000000000019' 133 | balBefore = usdc.balanceOf(weth_whale) 134 | swapexecutor.doOptimalSwapWithQuote(weth.address, usdc.address, sell_amount, (5, minOutput, [weth2USDCPoolId], []), {'from': weth_whale}) 135 | balAfter = usdc.balanceOf(weth_whale) 136 | assert (balAfter - balBefore) >= minOutput -------------------------------------------------------------------------------- /contracts/libraries/balancer/BalancerStableMath.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.7.6; 3 | pragma abicoder v2; 4 | 5 | import "./BalancerMath.sol"; 6 | import "./BalancerFixedPoint.sol"; 7 | 8 | // https://etherscan.io/address/0x7b50775383d3d6f0215a8f290f2c9e2eebbeceb2#code#F14#L25 9 | library BalancerStableMath { 10 | using BalancerFixedPoint for uint256; 11 | 12 | uint256 internal constant _AMP_PRECISION = 1e3; 13 | 14 | function _calculateInvariant(uint256 amplificationParameter, uint256[] memory balances, bool roundUp) internal pure returns (uint256) { 15 | /********************************************************************************************** 16 | // invariant // 17 | // D = invariant D^(n+1) // 18 | // A = amplification coefficient A n^n S + D = A D n^n + ----------- // 19 | // S = sum of balances n^n P // 20 | // P = product of balances // 21 | // n = number of tokens // 22 | **********************************************************************************************/ 23 | 24 | uint256 sum = 0; // S in the Curve version 25 | uint256 numTokens = balances.length; 26 | for (uint256 i = 0; i < numTokens; i++) { 27 | sum = sum.add(balances[i]); 28 | } 29 | if (sum == 0) { 30 | return 0; 31 | } 32 | 33 | uint256 prevInvariant = 0; 34 | uint256 invariant = sum; 35 | uint256 ampTimesTotal = amplificationParameter * numTokens; 36 | 37 | for (uint256 i = 0; i < 255; i++) { 38 | uint256 P_D = balances[0] * numTokens; 39 | for (uint256 j = 1; j < numTokens; j++) { 40 | P_D = BalancerMath.div(BalancerMath.mul(BalancerMath.mul(P_D, balances[j]), numTokens), invariant, roundUp); 41 | } 42 | prevInvariant = invariant; 43 | invariant = BalancerMath.div( 44 | BalancerMath.mul(BalancerMath.mul(numTokens, invariant), invariant).add( 45 | BalancerMath.div(BalancerMath.mul(BalancerMath.mul(ampTimesTotal, sum), P_D), _AMP_PRECISION, roundUp) 46 | ), 47 | BalancerMath.mul(numTokens + 1, invariant).add( 48 | // No need to use checked arithmetic for the amp precision, the amp is guaranteed to be at least 1 49 | BalancerMath.div(BalancerMath.mul(ampTimesTotal - _AMP_PRECISION, P_D), _AMP_PRECISION, !roundUp) 50 | ), 51 | roundUp 52 | ); 53 | 54 | if (invariant > prevInvariant) { 55 | if (invariant - prevInvariant <= 1) { 56 | return invariant; 57 | } 58 | } else if (prevInvariant - invariant <= 1) { 59 | return invariant; 60 | } 61 | } 62 | 63 | require(invariant < 0, '!INVT'); 64 | } 65 | 66 | function _getTokenBalanceGivenInvariantAndAllOtherBalances(uint256 amplificationParameter, uint256[] memory balances, uint256 invariant, uint256 tokenIndex) internal pure returns (uint256) { 67 | // Rounds result up overall 68 | 69 | uint256 ampTimesTotal = amplificationParameter * balances.length; 70 | uint256 sum = balances[0]; 71 | uint256 P_D = balances[0] * balances.length; 72 | for (uint256 j = 1; j < balances.length; j++) { 73 | P_D = BalancerMath.divDown(BalancerMath.mul(BalancerMath.mul(P_D, balances[j]), balances.length), invariant); 74 | sum = sum.add(balances[j]); 75 | } 76 | // No need to use safe math, based on the loop above `sum` is greater than or equal to `balances[tokenIndex]` 77 | sum = sum - balances[tokenIndex]; 78 | 79 | uint256 inv2 = BalancerMath.mul(invariant, invariant); 80 | // We remove the balance from c by multiplying it 81 | uint256 c = BalancerMath.mul( 82 | BalancerMath.mul(BalancerMath.divUp(inv2, BalancerMath.mul(ampTimesTotal, P_D)), _AMP_PRECISION), 83 | balances[tokenIndex] 84 | ); 85 | uint256 b = sum.add(BalancerMath.mul(BalancerMath.divDown(invariant, ampTimesTotal), _AMP_PRECISION)); 86 | 87 | // We iterate to find the balance 88 | uint256 prevTokenBalance = 0; 89 | // We multiply the first iteration outside the loop with the invariant to set the value of the 90 | // initial approximation. 91 | uint256 tokenBalance = BalancerMath.divUp(inv2.add(c), invariant.add(b)); 92 | 93 | for (uint256 i = 0; i < 255; i++) { 94 | prevTokenBalance = tokenBalance; 95 | 96 | tokenBalance = BalancerMath.divUp( 97 | BalancerMath.mul(tokenBalance, tokenBalance).add(c), 98 | BalancerMath.mul(tokenBalance, 2).add(b).sub(invariant) 99 | ); 100 | 101 | if (tokenBalance > prevTokenBalance) { 102 | if (tokenBalance - prevTokenBalance <= 1) { 103 | return tokenBalance; 104 | } 105 | } else if (prevTokenBalance - tokenBalance <= 1) { 106 | return tokenBalance; 107 | } 108 | } 109 | 110 | require(tokenBalance < 0, '!COVG'); 111 | } 112 | 113 | } -------------------------------------------------------------------------------- /contracts/archive/BasicOnChainPricingMainnetLenient.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-2.0 2 | pragma solidity 0.8.10; 3 | 4 | 5 | import {IERC20} from "@oz/token/ERC20/IERC20.sol"; 6 | import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol"; 7 | 8 | 9 | import "../../interfaces/uniswap/IUniswapRouterV2.sol"; 10 | import "../../interfaces/curve/ICurveRouter.sol"; 11 | 12 | // NOTE: Mostly here for archival 13 | // https://etherscan.io/address/0xbab7f98d62479309219b2cc5026d4ad1c6c05674#code 14 | // Used by VotiumBribesProcessor 15 | 16 | /// @title OnChainPricing 17 | /// @author Alex the Entreprenerd @ BadgerDAO 18 | /// @dev Pricer V1 19 | /// @dev Mainnet Version of Price Quoter, hardcoded for more efficiency 20 | /// @notice To spin a variant, just change the constants and use the Component Functions at the end of the file 21 | /// @notice Instead of upgrading in the future, just point to a new implementation 22 | /// @notice This version has 5% extra slippage to allow further flexibility 23 | /// if the manager abuses the check you should consider reverting back to a more rigorous pricer 24 | contract BasicOnChainPricingMainnetLenient { 25 | 26 | struct Quote { 27 | string name; 28 | uint256 amountOut; 29 | } 30 | 31 | // Assumption #1 Most tokens liquid pair is WETH (WETH is tokenized ETH for that chain) 32 | // e.g on Fantom, WETH would be wFTM 33 | address public constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; 34 | 35 | /// == Uni V2 Like Routers || These revert on non-existent pair == // 36 | // UniV2 37 | address public constant UNIV2_ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; // Spookyswap 38 | // Sushi 39 | address public constant SUSHI_ROUTER = 0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F; 40 | 41 | // Curve / Doesn't revert on failure 42 | address public constant CURVE_ROUTER = 0x8e764bE4288B842791989DB5b8ec067279829809; // Curve quote and swaps 43 | 44 | // === SLIPPAGE === // 45 | // Can change slippage within rational limits 46 | address public constant TECH_OPS = 0x86cbD0ce0c087b482782c181dA8d191De18C8275; 47 | 48 | uint256 private constant MAX_BPS = 10_000; 49 | 50 | uint256 private constant MAX_SLIPPAGE = 500; // 5% 51 | 52 | uint256 public slippage = 200; // 2% Initially 53 | 54 | 55 | function setSlippage(uint256 newSlippage) external { 56 | require(msg.sender == TECH_OPS, "Only TechOps"); 57 | require(newSlippage < MAX_SLIPPAGE); 58 | slippage = newSlippage; 59 | } 60 | 61 | // === PRICING === // 62 | 63 | /// @dev View function for testing the routing of the strategy 64 | function findOptimalSwap(address tokenIn, address tokenOut, uint256 amountIn) external returns (Quote memory) { 65 | uint256 length = 3; // Add length you need 66 | 67 | Quote[] memory quotes = new Quote[](length); 68 | 69 | uint256 curveQuote = getCurvePrice(CURVE_ROUTER, tokenIn, tokenOut, amountIn); 70 | quotes[0] = Quote("curve", curveQuote); 71 | 72 | uint256 uniQuote = getUniPrice(UNIV2_ROUTER, tokenIn, tokenOut, amountIn); 73 | quotes[1] = Quote("uniV2", uniQuote); 74 | 75 | uint256 sushiQuote = getUniPrice(SUSHI_ROUTER, tokenIn, tokenOut, amountIn); 76 | quotes[2] = Quote("sushi", sushiQuote); 77 | 78 | 79 | /// NOTE: Lack of Balancer and UniV3 80 | 81 | 82 | // Because this is a generalized contract, it is best to just loop, 83 | // Ideally we have a hierarchy for each chain to save some extra gas, but I think it's ok 84 | // O(n) complexity and each check is like 9 gas 85 | Quote memory bestQuote = quotes[0]; 86 | unchecked { 87 | for(uint256 x = 1; x < length; ++x) { 88 | if(quotes[x].amountOut > bestQuote.amountOut) { 89 | bestQuote = quotes[x]; 90 | } 91 | } 92 | } 93 | 94 | bestQuote.amountOut = bestQuote.amountOut * (MAX_BPS - slippage) / MAX_BPS; 95 | return bestQuote; 96 | } 97 | 98 | 99 | /// === Component Functions === /// 100 | /// Why bother? 101 | /// Because each chain is slightly different but most use similar tech / forks 102 | /// May as well use the separate functoions so each OnChain Pricing on different chains will be slightly different 103 | /// But ultimately will work in the same way 104 | 105 | /// @dev Given the address of the UniV2Like Router, the input amount, and the path, returns the quote for it 106 | function getUniPrice(address router, address tokenIn, address tokenOut, uint256 amountIn) public view returns (uint256) { 107 | address[] memory path = new address[](2); 108 | path[0] = address(tokenIn); 109 | path[1] = address(tokenOut); 110 | 111 | uint256 quote; //0 112 | 113 | // Specifically, test gas if we get revert vs if we check to avoid it 114 | try IUniswapRouterV2(router).getAmountsOut(amountIn, path) returns (uint256[] memory amounts) { 115 | quote = amounts[amounts.length - 1]; // Last one is the outToken 116 | } catch (bytes memory) { 117 | // We ignore as it means it's zero 118 | } 119 | 120 | return quote; 121 | } 122 | 123 | /// @dev Given the address of the CurveLike Router, the input amount, and the path, returns the quote for it 124 | function getCurvePrice(address router, address tokenIn, address tokenOut, uint256 amountIn) public view returns (uint256) { 125 | (, uint256 curveQuote) = ICurveRouter(router).get_best_rate(tokenIn, tokenOut, amountIn); 126 | 127 | return curveQuote; 128 | } 129 | } -------------------------------------------------------------------------------- /contracts/libraries/uniswap/SwapMath.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.7.6; 3 | pragma abicoder v2; 4 | 5 | import "./FullMath.sol"; 6 | import "./SqrtPriceMath.sol"; 7 | 8 | struct SwapExactInParam{ 9 | uint256 _amountIn; 10 | uint24 _fee; 11 | uint160 _currentPriceX96; 12 | uint160 _targetPriceX96; 13 | uint128 _liquidity; 14 | bool _zeroForOne; 15 | } 16 | 17 | // https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/SwapMath.sol 18 | library SwapMath { 19 | /// @notice Computes the result of swapping some amount in, or amount out, given the parameters of the swap 20 | /// @dev The fee, plus the amount in, will never exceed the amount remaining if the swap's `amountSpecified` is positive 21 | /// @param sqrtRatioCurrentX96 The current sqrt price of the pool 22 | /// @param sqrtRatioTargetX96 The price that cannot be exceeded, from which the direction of the swap is inferred 23 | /// @param liquidity The usable liquidity 24 | /// @param amountRemaining How much input or output amount is remaining to be swapped in/out 25 | /// @param feePips The fee taken from the input amount, expressed in hundredths of a bip 26 | /// @return sqrtRatioNextX96 The price after swapping the amount in/out, not to exceed the price target 27 | /// @return amountIn The amount to be swapped in, of either token0 or token1, based on the direction of the swap 28 | /// @return amountOut The amount to be received, of either token0 or token1, based on the direction of the swap 29 | /// @return feeAmount The amount of input that will be taken as a fee 30 | function computeSwapStep( 31 | uint160 sqrtRatioCurrentX96, 32 | uint160 sqrtRatioTargetX96, 33 | uint128 liquidity, 34 | int256 amountRemaining, 35 | uint24 feePips 36 | ) 37 | internal 38 | pure 39 | returns ( 40 | uint160 sqrtRatioNextX96, 41 | uint256 amountIn, 42 | uint256 amountOut, 43 | uint256 feeAmount 44 | ) 45 | { 46 | bool zeroForOne = sqrtRatioCurrentX96 >= sqrtRatioTargetX96; 47 | bool exactIn = amountRemaining >= 0; 48 | 49 | { 50 | if (exactIn) { 51 | SwapExactInParam memory _exactInParams = SwapExactInParam(uint256(amountRemaining), feePips, sqrtRatioCurrentX96, sqrtRatioTargetX96, liquidity, zeroForOne); 52 | (uint256 _amtIn, uint160 _nextPrice) = _getExactInNextPrice(_exactInParams); 53 | amountIn = _amtIn; 54 | sqrtRatioNextX96 = _nextPrice; 55 | } else { 56 | amountOut = zeroForOne 57 | ? SqrtPriceMath.getAmount1Delta(sqrtRatioTargetX96, sqrtRatioCurrentX96, liquidity, false) 58 | : SqrtPriceMath.getAmount0Delta(sqrtRatioCurrentX96, sqrtRatioTargetX96, liquidity, false); 59 | if (uint256(-amountRemaining) >= amountOut) sqrtRatioNextX96 = sqrtRatioTargetX96; 60 | else 61 | sqrtRatioNextX96 = SqrtPriceMath.getNextSqrtPriceFromOutput( 62 | sqrtRatioCurrentX96, 63 | liquidity, 64 | uint256(-amountRemaining), 65 | zeroForOne 66 | ); 67 | } 68 | } 69 | 70 | bool max = sqrtRatioTargetX96 == sqrtRatioNextX96; 71 | 72 | // get the input/output amounts 73 | { 74 | if (zeroForOne) { 75 | amountIn = max && exactIn 76 | ? amountIn 77 | : SqrtPriceMath.getAmount0Delta(sqrtRatioNextX96, sqrtRatioCurrentX96, liquidity, true); 78 | amountOut = max && !exactIn 79 | ? amountOut 80 | : SqrtPriceMath.getAmount1Delta(sqrtRatioNextX96, sqrtRatioCurrentX96, liquidity, false); 81 | }else { 82 | amountIn = max && exactIn 83 | ? amountIn 84 | : SqrtPriceMath.getAmount1Delta(sqrtRatioCurrentX96, sqrtRatioNextX96, liquidity, true); 85 | amountOut = max && !exactIn 86 | ? amountOut 87 | : SqrtPriceMath.getAmount0Delta(sqrtRatioCurrentX96, sqrtRatioNextX96, liquidity, false); 88 | } 89 | } 90 | 91 | // cap the output amount to not exceed the remaining output amount 92 | if (!exactIn && amountOut > uint256(-amountRemaining)) { 93 | amountOut = uint256(-amountRemaining); 94 | } 95 | 96 | if (exactIn && sqrtRatioNextX96 != sqrtRatioTargetX96) { 97 | // we didn't reach the target, so take the remainder of the maximum input as fee 98 | feeAmount = uint256(amountRemaining) - amountIn; 99 | } else { 100 | feeAmount = FullMath.mulDivRoundingUp(amountIn, feePips, 1e6 - feePips); 101 | } 102 | } 103 | 104 | function _getExactInNextPrice(SwapExactInParam memory _exactInParams) internal pure returns (uint256, uint160){ 105 | uint160 sqrtRatioNextX96; 106 | uint256 amountRemainingLessFee = FullMath.mulDiv(_exactInParams._amountIn, 1e6 - (_exactInParams._fee), 1e6); 107 | uint256 amountIn = _exactInParams._zeroForOne? SqrtPriceMath.getAmount0Delta(_exactInParams._targetPriceX96, _exactInParams._currentPriceX96, _exactInParams._liquidity, true) : 108 | SqrtPriceMath.getAmount1Delta(_exactInParams._currentPriceX96, _exactInParams._targetPriceX96, _exactInParams._liquidity, true); 109 | if (amountRemainingLessFee >= amountIn) sqrtRatioNextX96 = _exactInParams._targetPriceX96; 110 | else sqrtRatioNextX96 = SqrtPriceMath.getNextSqrtPriceFromInput(_exactInParams._currentPriceX96, _exactInParams._liquidity, amountRemainingLessFee, _exactInParams._zeroForOne); 111 | return (amountIn, sqrtRatioNextX96); 112 | } 113 | } -------------------------------------------------------------------------------- /tests/aura_processor/run_fuzz_real_deploys.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | from brownie import * 3 | from brownie.test import given, strategy 4 | import pytest 5 | 6 | """ 7 | Fuzz 8 | Fuzz any random address and amount 9 | To ensure no revert will happen 10 | 11 | ## This will take almost an hour. Consider using foundry :P 12 | """ 13 | LIVE_PROCESSOR = "0x8abd28e4d69bd3953b96dd9ed63533765adb9965" 14 | LIVE_PRICER = "0x2DC7693444aCd1EcA1D6dE5B3d0d8584F3870c49" 15 | 16 | 17 | 18 | ## TOKENS 19 | USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" 20 | 21 | 22 | ## Mostly Aura 23 | AURA = "0xC0c293ce456fF0ED870ADd98a0828Dd4d2903DBF" 24 | AURA_BAL = "0x616e8BfA43F920657B3497DBf40D6b1A02D4608d" 25 | 26 | BADGER = "0x3472A5A71965499acd81997a54BBA8D852C6E53d" 27 | 28 | SD = "0x30d20208d987713f46dfd34ef128bb16c404d10f" ## Pretty much completely new token https://etherscan.io/token/0x30d20208d987713f46dfd34ef128bb16c404d10f#balances 29 | 30 | DFX = "0x888888435FDe8e7d4c54cAb67f206e4199454c60" ## Fairly Liquid: https://etherscan.io/token/0x888888435FDe8e7d4c54cAb67f206e4199454c60#balances 31 | 32 | FDT = "0xEd1480d12bE41d92F36f5f7bDd88212E381A3677" ## Illiquid as of today, in vault but no pool I could find https://etherscan.io/token/0xEd1480d12bE41d92F36f5f7bDd88212E381A3677#balances 33 | 34 | LDO = "0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32" 35 | COW = "0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB" ## Has pair with GNO and with WETH 36 | GNO = "0x6810e776880C02933D47DB1b9fc05908e5386b96" 37 | 38 | ## Mostly Votium 39 | CVX = "0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B" 40 | WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" 41 | SNX = "0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F" 42 | TRIBE = "0xc7283b66Eb1EB5FB86327f08e1B5816b0720212B" 43 | FLX = "0x6243d8cea23066d098a15582d81a598b4e8391f4" 44 | INV = "0x41d5d79431a913c4ae7d69a668ecdfe5ff9dfb68" 45 | FXS = "0x3432B6A60D23Ca0dFCa7761B7ab56459D9C964D0" 46 | 47 | 48 | ## More Random Votium stuff 49 | TUSD = "0x0000000000085d4780B73119b644AE5ecd22b376" 50 | STG = "0xAf5191B0De278C7286d6C7CC6ab6BB8A73bA2Cd6" 51 | LYRA = "0x01BA67AAC7f75f647D94220Cc98FB30FCc5105Bf" 52 | JPEG = "0xE80C0cd204D654CEbe8dd64A4857cAb6Be8345a3" 53 | GRO = "0x3Ec8798B81485A254928B70CDA1cf0A2BB0B74D7" 54 | EURS = "0xdB25f211AB05b1c97D595516F45794528a807ad8" 55 | 56 | ## New Aura Pools 57 | DIGG = "0x798D1bE841a82a273720CE31c822C61a67a601C3" 58 | GRAVI_AURA = "0xBA485b556399123261a5F9c95d413B4f93107407" 59 | 60 | BRIBES_TOKEN_CLAIMABLE = [ 61 | ("0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B", 18), ## CVX 62 | ("0x6B175474E89094C44Da98b954EedeAC495271d0F", 18), ## DAI 63 | ("0x090185f2135308bad17527004364ebcc2d37e5f6", 22), ## SPELL ## NOTE: Using 22 to adjust as spell is super high supply 64 | ("0xdbdb4d16eda451d0503b854cf79d55697f90c8df", 18), ## ALCX 65 | ("0x9D79d5B61De59D882ce90125b18F74af650acB93", 8), ## NSBT ## NOTE: Using 6 + 2 decimals to make it more 66 | ("0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0", 18), ## MATIC 67 | ("0x3432b6a60d23ca0dfca7761b7ab56459d9c964d0", 18), ## FXS 68 | ("0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32", 18), ## LDO 69 | ("0xc7283b66Eb1EB5FB86327f08e1B5816b0720212B", 18), ## TRIBE 70 | ("0x8207c1FfC5B6804F6024322CcF34F29c3541Ae26", 18), ## OGN 71 | ("0xa3BeD4E1c75D00fa6f4E5E6922DB7261B5E9AcD2", 18), ## MTA 72 | ("0x31429d1856aD1377A8A0079410B297e1a9e214c2", 22), ## ANGLE ## NOTE Using 18 + 4 to raise the value 73 | ("0xCdF7028ceAB81fA0C6971208e83fa7872994beE5", 22), ## T ## NOTE Using 18 + 4 to raise the value 74 | ("0xa693B19d2931d498c5B318dF961919BB4aee87a5", 6), # UST 75 | ("0xB620Be8a1949AA9532e6a3510132864EF9Bc3F82", 22), ## LFT ## NOTE Using 18 + 4 to raise the value 76 | ("0x6243d8CEA23066d098a15582d81a598b4e8391F4", 18), ## FLX 77 | ("0x3Ec8798B81485A254928B70CDA1cf0A2BB0B74D7", 18), ## GRO 78 | ("0xAf5191B0De278C7286d6C7CC6ab6BB8A73bA2Cd6", 18), ## STG 79 | ("0xdB25f211AB05b1c97D595516F45794528a807ad8", 2), ## EURS 80 | ("0x674C6Ad92Fd080e4004b2312b45f796a192D27a0", 18), ## USDN 81 | ("0xFEEf77d3f69374f66429C91d732A244f074bdf74", 18), ## cvxFXS 82 | ("0x41D5D79431A913C4aE7d69a668ecdfE5fF9DFB68", 18), ## INV 83 | ("0xD33526068D116cE69F19A9ee46F0bd304F21A51f", 18), ## RPL 84 | (USDC, 6), 85 | (AURA, 18), 86 | (AURA_BAL, 18), 87 | (BADGER, 18), 88 | (SD, 18), ## Not Supported -> Cannot fix at this time 89 | (DFX, 18), 90 | (FDT, 18), ## Not Supported -> Cannot fix at this time 91 | (LDO, 18), 92 | (COW, 18), 93 | (GNO, 18), 94 | (CVX, 18), 95 | (SNX, 18), 96 | (TRIBE, 18), 97 | (FLX, 18), 98 | (INV, 18), 99 | (FXS, 18), 100 | 101 | ## More Coins 102 | (TUSD, 18), 103 | (STG, 18), 104 | (LYRA, 18), 105 | (JPEG, 18), 106 | (GRO, 18), 107 | (EURS, 18), 108 | 109 | ## From new Balancer Pools 110 | (DIGG, 9), 111 | (GRAVI_AURA, 18) 112 | ] 113 | 114 | 115 | ### Sell Bribes for Weth 116 | @given(amount=strategy("uint256"), sell_token_num=strategy("uint256")) 117 | def test_fuzz_processing(sell_token_num, amount): 118 | 119 | sell_token = interface.ERC20(BRIBES_TOKEN_CLAIMABLE[sell_token_num % len(BRIBES_TOKEN_CLAIMABLE)][0]) 120 | 121 | ## Skip if amt = 0 122 | if amount == 0: 123 | return True 124 | 125 | if str(web3.eth.getCode(str(sell_token.address))) == "b''": 126 | return True 127 | 128 | ## NOTE: We do not set anything as it's already been migrated 129 | setup_processor = AuraBribesProcessor.at(LIVE_PROCESSOR) 130 | 131 | 132 | settlement_fuzz = interface.ICowSettlement(setup_processor.SETTLEMENT()) 133 | 134 | if amount > sell_token.totalSupply(): 135 | amount = sell_token.totalSupply() - 1 ## Avoid revert due to insane numbers 136 | 137 | fee_amount = amount * 0.01 138 | data = [ 139 | sell_token, 140 | setup_processor.WETH(), ## Can only buy WETH here 141 | setup_processor.address, 142 | amount-fee_amount, 143 | 1.1579209e76, ## 2^256-1 / 10 so it passes 144 | 4294967294, 145 | "0x2B8694ED30082129598720860E8E972F07AA10D9B81CAE16CA0E2CFB24743E24", 146 | fee_amount, 147 | setup_processor.KIND_SELL(), 148 | False, 149 | setup_processor.BALANCE_ERC20(), 150 | setup_processor.BALANCE_ERC20() 151 | ] 152 | 153 | """ 154 | SKIP to avoid revert on these cases 155 | 156 | require(orderData.sellToken != AURA); // Can't sell AURA; 157 | require(orderData.sellToken != BADGER); // Can't sell BADGER either; 158 | require(orderData.sellToken != WETH); // Can't sell WETH 159 | require(orderData.buyToken == WETH); // Gotta Buy WETH; 160 | """ 161 | 162 | if sell_token == setup_processor.AURA(): 163 | return True 164 | if sell_token == setup_processor.BADGER(): 165 | return True 166 | if sell_token == setup_processor.WETH(): 167 | return True 168 | if sell_token == setup_processor.AURA(): 169 | return True 170 | 171 | 172 | uid = setup_processor.getOrderID(data) 173 | 174 | 175 | tx = setup_processor.sellBribeForWeth(data, uid, {"from": accounts.at(setup_processor.manager(), force=True)}) 176 | 177 | print("real test") 178 | 179 | assert settlement_fuzz.preSignature(uid) > 0 -------------------------------------------------------------------------------- /tests/on_chain_pricer/run_fuzz.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | from brownie import * 3 | from brownie.test import given, strategy 4 | import pytest 5 | 6 | """ 7 | Fuzz 8 | Fuzz of Pricer vs V2 Pricer for Equivalency 9 | """ 10 | 11 | ## TOKENS 12 | USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" 13 | 14 | ## Mostly Aura 15 | AURA = "0xC0c293ce456fF0ED870ADd98a0828Dd4d2903DBF" 16 | AURA_BAL = "0x616e8BfA43F920657B3497DBf40D6b1A02D4608d" 17 | BADGER = "0x3472A5A71965499acd81997a54BBA8D852C6E53d" 18 | 19 | SD = "0x30d20208d987713f46dfd34ef128bb16c404d10f" ## Pretty much completely new token https://etherscan.io/token/0x30d20208d987713f46dfd34ef128bb16c404d10f#balances 20 | DFX = "0x888888435FDe8e7d4c54cAb67f206e4199454c60" ## Fairly Liquid: https://etherscan.io/token/0x888888435FDe8e7d4c54cAb67f206e4199454c60#balances 21 | FDT = "0xEd1480d12bE41d92F36f5f7bDd88212E381A3677" ## Illiquid as of today, in vault but no pool I could find https://etherscan.io/token/0xEd1480d12bE41d92F36f5f7bDd88212E381A3677#balances 22 | LDO = "0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32" 23 | COW = "0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB" ## Has pair with GNO and with WETH 24 | GNO = "0x6810e776880C02933D47DB1b9fc05908e5386b96" 25 | 26 | ## Mostly Votium 27 | CVX = "0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B" 28 | WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" 29 | WBTC = "0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599" 30 | SNX = "0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F" 31 | TRIBE = "0xc7283b66Eb1EB5FB86327f08e1B5816b0720212B" 32 | FLX = "0x6243d8cea23066d098a15582d81a598b4e8391f4" 33 | INV = "0x41d5d79431a913c4ae7d69a668ecdfe5ff9dfb68" 34 | FXS = "0x3432B6A60D23Ca0dFCa7761B7ab56459D9C964D0" 35 | 36 | 37 | ## More Random Votium stuff 38 | TUSD = "0x0000000000085d4780B73119b644AE5ecd22b376" 39 | STG = "0xAf5191B0De278C7286d6C7CC6ab6BB8A73bA2Cd6" 40 | LYRA = "0x01BA67AAC7f75f647D94220Cc98FB30FCc5105Bf" 41 | JPEG = "0xE80C0cd204D654CEbe8dd64A4857cAb6Be8345a3" 42 | GRO = "0x3Ec8798B81485A254928B70CDA1cf0A2BB0B74D7" 43 | EURS = "0xdB25f211AB05b1c97D595516F45794528a807ad8" 44 | 45 | ## New Aura Pools 46 | DIGG = "0x798D1bE841a82a273720CE31c822C61a67a601C3" 47 | GRAVI_AURA = "0xBA485b556399123261a5F9c95d413B4f93107407" 48 | 49 | BRIBES_TOKEN_CLAIMABLE = [ 50 | ("0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B", 18), ## CVX 51 | ("0x6B175474E89094C44Da98b954EedeAC495271d0F", 18), ## DAI 52 | ("0x090185f2135308bad17527004364ebcc2d37e5f6", 22), ## SPELL ## NOTE: Using 22 to adjust as spell is super high supply 53 | ("0xdbdb4d16eda451d0503b854cf79d55697f90c8df", 18), ## ALCX 54 | ("0x9D79d5B61De59D882ce90125b18F74af650acB93", 8), ## NSBT ## NOTE: Using 6 + 2 decimals to make it more 55 | ("0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0", 18), ## MATIC 56 | ("0x3432b6a60d23ca0dfca7761b7ab56459d9c964d0", 18), ## FXS 57 | ("0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32", 18), ## LDO 58 | ("0xc7283b66Eb1EB5FB86327f08e1B5816b0720212B", 18), ## TRIBE 59 | ("0x8207c1FfC5B6804F6024322CcF34F29c3541Ae26", 18), ## OGN 60 | ("0xa3BeD4E1c75D00fa6f4E5E6922DB7261B5E9AcD2", 18), ## MTA 61 | ("0x31429d1856aD1377A8A0079410B297e1a9e214c2", 22), ## ANGLE ## NOTE Using 18 + 4 to raise the value 62 | ("0xCdF7028ceAB81fA0C6971208e83fa7872994beE5", 22), ## T ## NOTE Using 18 + 4 to raise the value 63 | ("0xa693B19d2931d498c5B318dF961919BB4aee87a5", 6), # UST 64 | ("0xB620Be8a1949AA9532e6a3510132864EF9Bc3F82", 22), ## LFT ## NOTE Using 18 + 4 to raise the value 65 | ("0x6243d8CEA23066d098a15582d81a598b4e8391F4", 18), ## FLX 66 | ("0x3Ec8798B81485A254928B70CDA1cf0A2BB0B74D7", 18), ## GRO 67 | ("0xAf5191B0De278C7286d6C7CC6ab6BB8A73bA2Cd6", 18), ## STG 68 | ("0xdB25f211AB05b1c97D595516F45794528a807ad8", 2), ## EURS 69 | ("0x674C6Ad92Fd080e4004b2312b45f796a192D27a0", 18), ## USDN 70 | ("0xFEEf77d3f69374f66429C91d732A244f074bdf74", 18), ## cvxFXS 71 | ("0x41D5D79431A913C4aE7d69a668ecdfE5fF9DFB68", 18), ## INV 72 | ("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", 6), ## USDC 73 | ("0xC0c293ce456fF0ED870ADd98a0828Dd4d2903DBF", 18), ## AURA 74 | ("0x616e8BfA43F920657B3497DBf40D6b1A02D4608d", 18), ## AURA_BAL 75 | ("0x3472A5A71965499acd81997a54BBA8D852C6E53d", 18), ## BADGER 76 | ("0x30d20208d987713f46dfd34ef128bb16c404d10f", 18), ## SD | Not Supported -> Cannot fix at this time 77 | ("0x888888435FDe8e7d4c54cAb67f206e4199454c60", 18), ## DFX 78 | ("0xEd1480d12bE41d92F36f5f7bDd88212E381A3677", 18), ## FDT ## Not Supported -> Cannot fix at this time 79 | ("0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32", 18), ## LDO 80 | ("0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB", 18), ## COW 81 | ("0x6810e776880C02933D47DB1b9fc05908e5386b96", 18), ## GNO 82 | ("0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B", 18), ## CVX 83 | ("0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F", 18), ## SNX 84 | ("0xc7283b66Eb1EB5FB86327f08e1B5816b0720212B", 18), ## TRIBE 85 | ("0x6243d8cea23066d098a15582d81a598b4e8391f4", 18), ## FLX 86 | ("0x41d5d79431a913c4ae7d69a668ecdfe5ff9dfb68", 18), ## INV 87 | ("0x3432B6A60D23Ca0dFCa7761B7ab56459D9C964D0", 18), ## FXS 88 | 89 | ("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", 18), ## WETH 90 | ("0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599", 8), ## WBTC 91 | 92 | ## More Coins 93 | ("0x0000000000085d4780B73119b644AE5ecd22b376", 18), ## TUSD 94 | ("0xAf5191B0De278C7286d6C7CC6ab6BB8A73bA2Cd6", 18), ## STG 95 | ("0x01BA67AAC7f75f647D94220Cc98FB30FCc5105Bf", 18), ## LYRA 96 | ("0xE80C0cd204D654CEbe8dd64A4857cAb6Be8345a3", 18), ## JPEG 97 | ("0x3Ec8798B81485A254928B70CDA1cf0A2BB0B74D7", 18), ## GRO 98 | ("0xdB25f211AB05b1c97D595516F45794528a807ad8", 18), ## EURS 99 | 100 | ## From new Balancer Pools 101 | ("0x798D1bE841a82a273720CE31c822C61a67a601C3", 9), ## DIGG 102 | ("0xBA485b556399123261a5F9c95d413B4f93107407", 18) ## GRAVI_AURA 103 | ] 104 | 105 | 106 | ### Sell Bribes for Weth 107 | @given(amount=strategy("uint256"), sell_token_num=strategy("uint256"), buy_token_num=strategy("uint256")) 108 | def test_fuzz_pricers(sell_token_num, buy_token_num, amount): 109 | 110 | sell_token = interface.ERC20(BRIBES_TOKEN_CLAIMABLE[sell_token_num % len(BRIBES_TOKEN_CLAIMABLE)][0]) 111 | buy_token = interface.ERC20(BRIBES_TOKEN_CLAIMABLE[buy_token_num % len(BRIBES_TOKEN_CLAIMABLE)][0]) 112 | 113 | ## Skip if amt = 0 114 | if amount == 0: 115 | return True 116 | 117 | ## Fake Token = Skip 118 | if str(web3.eth.getCode(str(sell_token.address))) == "b''": 119 | return True 120 | 121 | 122 | ## NOTE: Put all the fixtures here cause I keep getting reverts 123 | #### FIXTURES ### 124 | 125 | ## NOTE: We have 5% slippage on this one 126 | univ3simulator = UniV3SwapSimulator.deploy({"from": accounts[0]}) 127 | balancerV2Simulator = BalancerSwapSimulator.deploy({"from": accounts[0]}) 128 | pricer_fuzz_v3 = OnChainPricingMainnet.deploy(univ3simulator.address, balancerV2Simulator.address, {"from": accounts[0]}) 129 | 130 | pricer_fuzz_v2 = FullOnChainPricingMainnet.deploy({"from": a[0]}) 131 | 132 | try: 133 | v2_quote = pricer_fuzz_v2.findOptimalSwap.call(sell_token, buy_token, amount) 134 | except: 135 | print("Exception from V2") 136 | 137 | v3_quote = pricer_fuzz_v3.findOptimalSwap(sell_token, buy_token, amount) 138 | 139 | ## Compare quote.amountOut 140 | ## >= for equivalent or better value for any combination 141 | ## NOTE: Have had a test fail by 1 wei, due to rounding prob 142 | # assert v3_quote[1] >= v2_quote[1] 143 | assert v3_quote[1] >= 0 ## Did not revert -------------------------------------------------------------------------------- /tests/aura_processor/run_fuzz_integration_preview.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | from brownie import * 3 | from brownie.test import given, strategy 4 | import pytest 5 | 6 | """ 7 | Fuzz 8 | Fuzz any random address and amount 9 | To ensure no revert will happen 10 | 11 | Fuzz Test with Cowswap Orders for Live Setup 12 | 13 | For future migrations use other tests 14 | """ 15 | LIVE_PROCESSOR = "0x8abd28e4d69bd3953b96dd9ed63533765adb9965" 16 | LIVE_PRICER = "0x2DC7693444aCd1EcA1D6dE5B3d0d8584F3870c49" 17 | 18 | 19 | 20 | ## TOKENS 21 | USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" 22 | 23 | 24 | ## Mostly Aura 25 | AURA = "0xC0c293ce456fF0ED870ADd98a0828Dd4d2903DBF" 26 | AURA_BAL = "0x616e8BfA43F920657B3497DBf40D6b1A02D4608d" 27 | 28 | BADGER = "0x3472A5A71965499acd81997a54BBA8D852C6E53d" 29 | 30 | SD = "0x30d20208d987713f46dfd34ef128bb16c404d10f" ## Pretty much completely new token https://etherscan.io/token/0x30d20208d987713f46dfd34ef128bb16c404d10f#balances 31 | 32 | DFX = "0x888888435FDe8e7d4c54cAb67f206e4199454c60" ## Fairly Liquid: https://etherscan.io/token/0x888888435FDe8e7d4c54cAb67f206e4199454c60#balances 33 | 34 | FDT = "0xEd1480d12bE41d92F36f5f7bDd88212E381A3677" ## Illiquid as of today, in vault but no pool I could find https://etherscan.io/token/0xEd1480d12bE41d92F36f5f7bDd88212E381A3677#balances 35 | 36 | LDO = "0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32" 37 | COW = "0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB" ## Has pair with GNO and with WETH 38 | GNO = "0x6810e776880C02933D47DB1b9fc05908e5386b96" 39 | 40 | ## Mostly Votium 41 | CVX = "0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B" 42 | WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" 43 | SNX = "0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F" 44 | TRIBE = "0xc7283b66Eb1EB5FB86327f08e1B5816b0720212B" 45 | FLX = "0x6243d8cea23066d098a15582d81a598b4e8391f4" 46 | INV = "0x41d5d79431a913c4ae7d69a668ecdfe5ff9dfb68" 47 | FXS = "0x3432B6A60D23Ca0dFCa7761B7ab56459D9C964D0" 48 | 49 | 50 | ## More Random Votium stuff 51 | TUSD = "0x0000000000085d4780B73119b644AE5ecd22b376" 52 | STG = "0xAf5191B0De278C7286d6C7CC6ab6BB8A73bA2Cd6" 53 | LYRA = "0x01BA67AAC7f75f647D94220Cc98FB30FCc5105Bf" 54 | JPEG = "0xE80C0cd204D654CEbe8dd64A4857cAb6Be8345a3" 55 | GRO = "0x3Ec8798B81485A254928B70CDA1cf0A2BB0B74D7" 56 | EURS = "0xdB25f211AB05b1c97D595516F45794528a807ad8" 57 | 58 | ## New Aura Pools 59 | DIGG = "0x798D1bE841a82a273720CE31c822C61a67a601C3" 60 | GRAVI_AURA = "0xBA485b556399123261a5F9c95d413B4f93107407" 61 | 62 | BRIBES_TOKEN_CLAIMABLE = [ 63 | ("0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B", 18), ## CVX 64 | ("0x6B175474E89094C44Da98b954EedeAC495271d0F", 18), ## DAI 65 | ("0x090185f2135308bad17527004364ebcc2d37e5f6", 22), ## SPELL ## NOTE: Using 22 to adjust as spell is super high supply 66 | ("0xdbdb4d16eda451d0503b854cf79d55697f90c8df", 18), ## ALCX 67 | ("0x9D79d5B61De59D882ce90125b18F74af650acB93", 8), ## NSBT ## NOTE: Using 6 + 2 decimals to make it more 68 | ("0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0", 18), ## MATIC 69 | ("0x3432b6a60d23ca0dfca7761b7ab56459d9c964d0", 18), ## FXS 70 | ("0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32", 18), ## LDO 71 | ("0xc7283b66Eb1EB5FB86327f08e1B5816b0720212B", 18), ## TRIBE 72 | ("0x8207c1FfC5B6804F6024322CcF34F29c3541Ae26", 18), ## OGN 73 | ("0xa3BeD4E1c75D00fa6f4E5E6922DB7261B5E9AcD2", 18), ## MTA 74 | ("0x31429d1856aD1377A8A0079410B297e1a9e214c2", 22), ## ANGLE ## NOTE Using 18 + 4 to raise the value 75 | ("0xCdF7028ceAB81fA0C6971208e83fa7872994beE5", 22), ## T ## NOTE Using 18 + 4 to raise the value 76 | ("0xa693B19d2931d498c5B318dF961919BB4aee87a5", 6), # UST 77 | ("0xB620Be8a1949AA9532e6a3510132864EF9Bc3F82", 22), ## LFT ## NOTE Using 18 + 4 to raise the value 78 | ("0x6243d8CEA23066d098a15582d81a598b4e8391F4", 18), ## FLX 79 | ("0x3Ec8798B81485A254928B70CDA1cf0A2BB0B74D7", 18), ## GRO 80 | ("0xAf5191B0De278C7286d6C7CC6ab6BB8A73bA2Cd6", 18), ## STG 81 | ("0xdB25f211AB05b1c97D595516F45794528a807ad8", 2), ## EURS 82 | ("0x674C6Ad92Fd080e4004b2312b45f796a192D27a0", 18), ## USDN 83 | ("0xFEEf77d3f69374f66429C91d732A244f074bdf74", 18), ## cvxFXS 84 | ("0x41D5D79431A913C4aE7d69a668ecdfE5fF9DFB68", 18), ## INV 85 | (USDC, 6), 86 | (AURA, 18), 87 | (AURA_BAL, 18), 88 | (BADGER, 18), 89 | (SD, 18), ## Not Supported -> Cannot fix at this time 90 | (DFX, 18), 91 | (FDT, 18), ## Not Supported -> Cannot fix at this time 92 | (LDO, 18), 93 | (COW, 18), 94 | (GNO, 18), 95 | (CVX, 18), 96 | (SNX, 18), 97 | (TRIBE, 18), 98 | (FLX, 18), 99 | (INV, 18), 100 | (FXS, 18), 101 | 102 | ## More Coins 103 | (TUSD, 18), 104 | (STG, 18), 105 | (LYRA, 18), 106 | (JPEG, 18), 107 | (GRO, 18), 108 | (EURS, 18), 109 | 110 | ## From new Balancer Pools 111 | (DIGG, 9), 112 | (GRAVI_AURA, 18) 113 | ] 114 | 115 | 116 | ### Sell Bribes for Weth 117 | @given(amount=strategy("uint256"), sell_token_num=strategy("uint256")) 118 | def test_fuzz_processing(sell_token_num, amount): 119 | 120 | sell_token = interface.ERC20(BRIBES_TOKEN_CLAIMABLE[sell_token_num % len(BRIBES_TOKEN_CLAIMABLE)][0]) 121 | 122 | ## Skip if amt = 0 123 | if amount == 0: 124 | return True 125 | 126 | if str(web3.eth.getCode(str(sell_token.address))) == "b''": 127 | return True 128 | 129 | 130 | ## NOTE: Put all the fixtures here cause I keep getting reverts 131 | #### FIXTURES ### 132 | 133 | ## NOTE: We have 5% slippage on this one 134 | lenient_pricer_fuzz = OnChainPricingMainnetLenient.at(LIVE_PRICER) 135 | lenient_pricer_fuzz.setSlippage(499, {"from": accounts.at(lenient_pricer_fuzz.TECH_OPS(), force=True)}) 136 | 137 | setup_processor = AuraBribesProcessor.at(LIVE_PROCESSOR) 138 | 139 | dev_multi = accounts.at(setup_processor.DEV_MULTI(), force=True) 140 | setup_processor.setPricer(lenient_pricer_fuzz, {"from": dev_multi}) 141 | 142 | 143 | settlement_fuzz = interface.ICowSettlement(setup_processor.SETTLEMENT()) 144 | 145 | if amount > sell_token.totalSupply(): 146 | amount = sell_token.totalSupply() - 1 ## Avoid revert due to insane numbers 147 | 148 | fee_amount = amount * 0.01 149 | data = [ 150 | sell_token, 151 | setup_processor.WETH(), ## Can only buy WETH here 152 | setup_processor.address, 153 | amount-fee_amount, 154 | 1.1579209e76, ## 2^256-1 / 10 so it passes 155 | 4294967294, 156 | "0x2B8694ED30082129598720860E8E972F07AA10D9B81CAE16CA0E2CFB24743E24", 157 | fee_amount, 158 | setup_processor.KIND_SELL(), 159 | False, 160 | setup_processor.BALANCE_ERC20(), 161 | setup_processor.BALANCE_ERC20() 162 | ] 163 | 164 | """ 165 | SKIP to avoid revert on these cases 166 | 167 | require(orderData.sellToken != AURA); // Can't sell AURA; 168 | require(orderData.sellToken != BADGER); // Can't sell BADGER either; 169 | require(orderData.sellToken != WETH); // Can't sell WETH 170 | require(orderData.buyToken == WETH); // Gotta Buy WETH; 171 | """ 172 | 173 | if sell_token == setup_processor.AURA(): 174 | return True 175 | if sell_token == setup_processor.BADGER(): 176 | return True 177 | if sell_token == setup_processor.WETH(): 178 | return True 179 | if sell_token == setup_processor.AURA(): 180 | return True 181 | 182 | 183 | uid = setup_processor.getOrderID(data) 184 | 185 | 186 | tx = setup_processor.sellBribeForWeth(data, uid, {"from": accounts.at(setup_processor.manager(), force=True)}) 187 | 188 | print("real test") 189 | 190 | assert settlement_fuzz.preSignature(uid) > 0 -------------------------------------------------------------------------------- /tests/aura_processor/run_fuzz.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | from brownie import * 3 | from brownie.test import given, strategy 4 | import pytest 5 | 6 | """ 7 | Fuzz 8 | Fuzz any random address and amount 9 | To ensure no revert will happen 10 | 11 | ## This will take almost an hour. Consider using foundry :P 12 | """ 13 | LIVE_PROCESSOR = "0x8abd28e4d69bd3953b96dd9ed63533765adb9965" 14 | 15 | 16 | ## TOKENS 17 | USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" 18 | 19 | 20 | ## Mostly Aura 21 | AURA = "0xC0c293ce456fF0ED870ADd98a0828Dd4d2903DBF" 22 | AURA_BAL = "0x616e8BfA43F920657B3497DBf40D6b1A02D4608d" 23 | 24 | BADGER = "0x3472A5A71965499acd81997a54BBA8D852C6E53d" 25 | 26 | SD = "0x30d20208d987713f46dfd34ef128bb16c404d10f" ## Pretty much completely new token https://etherscan.io/token/0x30d20208d987713f46dfd34ef128bb16c404d10f#balances 27 | 28 | DFX = "0x888888435FDe8e7d4c54cAb67f206e4199454c60" ## Fairly Liquid: https://etherscan.io/token/0x888888435FDe8e7d4c54cAb67f206e4199454c60#balances 29 | 30 | FDT = "0xEd1480d12bE41d92F36f5f7bDd88212E381A3677" ## Illiquid as of today, in vault but no pool I could find https://etherscan.io/token/0xEd1480d12bE41d92F36f5f7bDd88212E381A3677#balances 31 | 32 | LDO = "0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32" 33 | COW = "0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB" ## Has pair with GNO and with WETH 34 | GNO = "0x6810e776880C02933D47DB1b9fc05908e5386b96" 35 | 36 | ## Mostly Votium 37 | CVX = "0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B" 38 | WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" 39 | SNX = "0xC011a73ee8576Fb46F5E1c5751cA3B9Fe0af2a6F" 40 | TRIBE = "0xc7283b66Eb1EB5FB86327f08e1B5816b0720212B" 41 | FLX = "0x6243d8cea23066d098a15582d81a598b4e8391f4" 42 | INV = "0x41d5d79431a913c4ae7d69a668ecdfe5ff9dfb68" 43 | FXS = "0x3432B6A60D23Ca0dFCa7761B7ab56459D9C964D0" 44 | 45 | 46 | ## More Random Votium stuff 47 | TUSD = "0x0000000000085d4780B73119b644AE5ecd22b376" 48 | STG = "0xAf5191B0De278C7286d6C7CC6ab6BB8A73bA2Cd6" 49 | LYRA = "0x01BA67AAC7f75f647D94220Cc98FB30FCc5105Bf" 50 | JPEG = "0xE80C0cd204D654CEbe8dd64A4857cAb6Be8345a3" 51 | GRO = "0x3Ec8798B81485A254928B70CDA1cf0A2BB0B74D7" 52 | EURS = "0xdB25f211AB05b1c97D595516F45794528a807ad8" 53 | 54 | ## New Aura Pools 55 | DIGG = "0x798D1bE841a82a273720CE31c822C61a67a601C3" 56 | GRAVI_AURA = "0xBA485b556399123261a5F9c95d413B4f93107407" 57 | 58 | BRIBES_TOKEN_CLAIMABLE = [ 59 | ("0x4e3FBD56CD56c3e72c1403e103b45Db9da5B9D2B", 18), ## CVX 60 | ("0x6B175474E89094C44Da98b954EedeAC495271d0F", 18), ## DAI 61 | ("0x090185f2135308bad17527004364ebcc2d37e5f6", 22), ## SPELL ## NOTE: Using 22 to adjust as spell is super high supply 62 | ("0xdbdb4d16eda451d0503b854cf79d55697f90c8df", 18), ## ALCX 63 | ("0x9D79d5B61De59D882ce90125b18F74af650acB93", 8), ## NSBT ## NOTE: Using 6 + 2 decimals to make it more 64 | ("0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0", 18), ## MATIC 65 | ("0x3432b6a60d23ca0dfca7761b7ab56459d9c964d0", 18), ## FXS 66 | ("0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32", 18), ## LDO 67 | ("0xc7283b66Eb1EB5FB86327f08e1B5816b0720212B", 18), ## TRIBE 68 | ("0x8207c1FfC5B6804F6024322CcF34F29c3541Ae26", 18), ## OGN 69 | ("0xa3BeD4E1c75D00fa6f4E5E6922DB7261B5E9AcD2", 18), ## MTA 70 | ("0x31429d1856aD1377A8A0079410B297e1a9e214c2", 22), ## ANGLE ## NOTE Using 18 + 4 to raise the value 71 | ("0xCdF7028ceAB81fA0C6971208e83fa7872994beE5", 22), ## T ## NOTE Using 18 + 4 to raise the value 72 | ("0xa693B19d2931d498c5B318dF961919BB4aee87a5", 6), # UST 73 | ("0xB620Be8a1949AA9532e6a3510132864EF9Bc3F82", 22), ## LFT ## NOTE Using 18 + 4 to raise the value 74 | ("0x6243d8CEA23066d098a15582d81a598b4e8391F4", 18), ## FLX 75 | ("0x3Ec8798B81485A254928B70CDA1cf0A2BB0B74D7", 18), ## GRO 76 | ("0xAf5191B0De278C7286d6C7CC6ab6BB8A73bA2Cd6", 18), ## STG 77 | ("0xdB25f211AB05b1c97D595516F45794528a807ad8", 2), ## EURS 78 | ("0x674C6Ad92Fd080e4004b2312b45f796a192D27a0", 18), ## USDN 79 | ("0xFEEf77d3f69374f66429C91d732A244f074bdf74", 18), ## cvxFXS 80 | ("0x41D5D79431A913C4aE7d69a668ecdfE5fF9DFB68", 18), ## INV 81 | (USDC, 6), 82 | (AURA, 18), 83 | (AURA_BAL, 18), 84 | (BADGER, 18), 85 | (SD, 18), ## Not Supported -> Cannot fix at this time 86 | (DFX, 18), 87 | (FDT, 18), ## Not Supported -> Cannot fix at this time 88 | (LDO, 18), 89 | (COW, 18), 90 | (GNO, 18), 91 | (CVX, 18), 92 | (SNX, 18), 93 | (TRIBE, 18), 94 | (FLX, 18), 95 | (INV, 18), 96 | (FXS, 18), 97 | 98 | ## More Coins 99 | (TUSD, 18), 100 | (STG, 18), 101 | (LYRA, 18), 102 | (JPEG, 18), 103 | (GRO, 18), 104 | (EURS, 18), 105 | 106 | ## From new Balancer Pools 107 | (DIGG, 9), 108 | (GRAVI_AURA, 18) 109 | ] 110 | 111 | 112 | ### Sell Bribes for Weth 113 | @given(amount=strategy("uint256"), sell_token_num=strategy("uint256")) 114 | def test_fuzz_processing(sell_token_num, amount): 115 | 116 | sell_token = interface.ERC20(BRIBES_TOKEN_CLAIMABLE[sell_token_num % len(BRIBES_TOKEN_CLAIMABLE)][0]) 117 | 118 | ## Skip if amt = 0 119 | if amount == 0: 120 | return True 121 | 122 | if str(web3.eth.getCode(str(sell_token.address))) == "b''": 123 | return True 124 | 125 | 126 | ## NOTE: Put all the fixtures here cause I keep getting reverts 127 | #### FIXTURES ### 128 | 129 | ## NOTE: We have 5% slippage on this one 130 | univ3simulator = UniV3SwapSimulator.deploy({"from": accounts[0]}) 131 | balancerV2Simulator = BalancerSwapSimulator.deploy({"from": accounts[0]}) 132 | lenient_pricer_fuzz = OnChainPricingMainnetLenient.deploy(univ3simulator.address, balancerV2Simulator.address, {"from": accounts[0]}) 133 | lenient_pricer_fuzz.setSlippage(499, {"from": accounts.at(lenient_pricer_fuzz.TECH_OPS(), force=True)}) 134 | 135 | setup_processor = AuraBribesProcessor.at(LIVE_PROCESSOR) 136 | 137 | dev_multi = accounts.at(setup_processor.DEV_MULTI(), force=True) 138 | setup_processor.setPricer(lenient_pricer_fuzz, {"from": dev_multi}) 139 | 140 | 141 | settlement_fuzz = interface.ICowSettlement(setup_processor.SETTLEMENT()) 142 | 143 | if amount > sell_token.totalSupply(): 144 | amount = sell_token.totalSupply() - 1 ## Avoid revert due to insane numbers 145 | 146 | fee_amount = amount * 0.01 147 | data = [ 148 | sell_token, 149 | setup_processor.WETH(), ## Can only buy WETH here 150 | setup_processor.address, 151 | amount-fee_amount, 152 | 1.1579209e76, ## 2^256-1 / 10 so it passes 153 | 4294967294, 154 | "0x2B8694ED30082129598720860E8E972F07AA10D9B81CAE16CA0E2CFB24743E24", 155 | fee_amount, 156 | setup_processor.KIND_SELL(), 157 | False, 158 | setup_processor.BALANCE_ERC20(), 159 | setup_processor.BALANCE_ERC20() 160 | ] 161 | 162 | """ 163 | SKIP to avoid revert on these cases 164 | 165 | require(orderData.sellToken != AURA); // Can't sell AURA; 166 | require(orderData.sellToken != BADGER); // Can't sell BADGER either; 167 | require(orderData.sellToken != WETH); // Can't sell WETH 168 | require(orderData.buyToken == WETH); // Gotta Buy WETH; 169 | """ 170 | 171 | if sell_token == setup_processor.AURA(): 172 | return True 173 | if sell_token == setup_processor.BADGER(): 174 | return True 175 | if sell_token == setup_processor.WETH(): 176 | return True 177 | if sell_token == setup_processor.AURA(): 178 | return True 179 | 180 | 181 | uid = setup_processor.getOrderID(data) 182 | 183 | 184 | tx = setup_processor.sellBribeForWeth(data, uid, {"from": accounts.at(setup_processor.manager(), force=True)}) 185 | 186 | print("real test") 187 | 188 | assert settlement_fuzz.preSignature(uid) > 0 -------------------------------------------------------------------------------- /tests/heuristic_equivalency/test_heuristic_equivalency.py: -------------------------------------------------------------------------------- 1 | from rich.console import Console 2 | 3 | import pytest 4 | console = Console() 5 | 6 | """ 7 | Evaluates the pricing quotes generated by the optimized (heuristic) version of the OnChainPricingMainnet 8 | in contrast to its legacy version. The new version should lead to the same optimal price while consuming 9 | less gas. 10 | 11 | Tests excluded from main test suite as core functionalities are not tested here. In order to add to test 12 | suite, modify the file name to: `test_heuristic_equivalency.py`. Note that tested routes depend on current 13 | liquidity state and, if liquidity moves between protocols, some assertions may fail. 14 | """ 15 | 16 | ### Test findOptimalSwap Equivalencies for different cases 17 | def test_pricing_equivalency_uniswap_v2(weth, pricerwrapper, pricer_legacy): 18 | pricer = pricerwrapper 19 | token = "0xBC7250C8c3eCA1DfC1728620aF835FCa489bFdf3" # some swap (GM-WETH) only in Uniswap V2 20 | ## 1e18 21 | sell_count = 100000000 22 | sell_amount = sell_count * 1000000000 ## 1e9 23 | 24 | tx = pricer.findOptimalSwap(token, weth.address, sell_amount) 25 | assert tx[1][0] == 1 ## UNIV2 26 | quote = tx[1][1] 27 | 28 | tx2 = pricer_legacy.findOptimalSwap(token, weth.address, sell_amount) 29 | assert tx2.return_value[0] == 1 ## UNIV2 30 | quote_legacy = tx2.return_value[1] 31 | 32 | assert quote >= quote_legacy # Optimized quote must be the same or better 33 | assert tx[0] > 0 and tx[0] < tx2.gas_used 34 | 35 | def test_pricing_equivalency_uniswap_v2_sushi(oneE18, weth, pricerwrapper, pricer_legacy): 36 | pricer = pricerwrapper 37 | token = "0x2e9d63788249371f1DFC918a52f8d799F4a38C94" # some swap (TOKE-WETH) only in Uniswap V2 & SushiSwap 38 | ## 1e18 39 | sell_count = 5000 40 | sell_amount = sell_count * oneE18 ## 1e18 41 | 42 | tx = pricer.findOptimalSwap(token, weth.address, sell_amount) 43 | assert (tx[1][0] == 1 or tx[1][0] == 2) ## UNIV2 or SUSHI 44 | quote = tx[1][1] 45 | 46 | tx2 = pricer_legacy.findOptimalSwap(token, weth.address, sell_amount) 47 | assert (tx2.return_value[0] == 1 or tx2.return_value[0] == 2) ## UNIV2 or SUSHI 48 | quote_legacy = tx2.return_value[1] 49 | 50 | assert quote >= quote_legacy # Optimized quote must be the same or better 51 | assert tx[0] > 0 and tx[0] < tx2.gas_used 52 | 53 | def test_pricing_equivalency_balancer_v2(oneE18, weth, aura, pricerwrapper, pricer_legacy): 54 | pricer = pricerwrapper 55 | token = aura # some swap (AURA-WETH) only in Balancer V2 56 | ## 1e18 57 | sell_count = 8000 58 | sell_amount = sell_count * oneE18 ## 1e18 59 | 60 | tx = pricer.findOptimalSwap(token, weth.address, sell_amount) 61 | assert tx[1][0] == 5 ## BALANCER 62 | quote = tx[1][1] 63 | 64 | tx2 = pricer_legacy.findOptimalSwap(token, weth.address, sell_amount) 65 | assert tx2.return_value[0] == 5 ## BALANCER 66 | quote_legacy = tx2.return_value[1] 67 | 68 | assert quote >= quote_legacy # Optimized quote must be the same or better 69 | assert tx[0] > 0 and tx[0] < tx2.gas_used 70 | 71 | def test_pricing_equivalency_balancer_v2_with_weth(oneE18, wbtc, aura, pricerwrapper, pricer_legacy): 72 | pricer = pricerwrapper 73 | token = aura # some swap (AURA-WETH-WBTC) only in Balancer V2 via WETH in between as connector 74 | ## 1e18 75 | sell_count = 8000 76 | sell_amount = sell_count * oneE18 ## 1e18 77 | 78 | tx = pricer.findOptimalSwap(token, wbtc.address, sell_amount) 79 | assert tx[1][0] == 6 ## BALANCERWITHWETH 80 | quote = tx[1][1] 81 | 82 | tx2 = pricer_legacy.findOptimalSwap(token, wbtc.address, sell_amount) 83 | assert tx2.return_value[0] == 6 ## BALANCERWITHWETH 84 | quote_legacy = tx2.return_value[1] 85 | 86 | assert quote >= quote_legacy # Optimized quote must be the same or better 87 | assert tx[0] > 0 and tx[0] < tx2.gas_used 88 | 89 | def test_pricing_equivalency_uniswap_v3(oneE18, weth, pricerwrapper, pricer_legacy): 90 | pricer = pricerwrapper 91 | token = "0xf4d2888d29D722226FafA5d9B24F9164c092421E" # some swap (LOOKS-WETH) only in Uniswap V3 92 | ## 1e18 93 | sell_count = 600000 94 | sell_amount = sell_count * oneE18 ## 1e18 95 | 96 | tx = pricer.findOptimalSwap(token, weth.address, sell_amount) 97 | assert tx[1][0] == 3 ## UNIV3 98 | quote = tx[1][1] 99 | 100 | tx2 = pricer_legacy.findOptimalSwap(token, weth.address, sell_amount) 101 | assert tx2.return_value[0] == 3 ## UNIV3 102 | quote_legacy = tx2.return_value[1] 103 | 104 | assert quote >= quote_legacy # Optimized quote must be the same or better 105 | assert tx[0] > 0 and tx[0] < tx2.gas_used 106 | 107 | def test_pricing_equivalency_uniswap_v3_with_weth(oneE18, wbtc, pricerwrapper, pricer_legacy): 108 | pricer = pricerwrapper 109 | token = "0xf4d2888d29D722226FafA5d9B24F9164c092421E" # some swap (LOOKS-WETH-WBTC) only in Uniswap V3 via WETH in between as connector 110 | ## 1e18 111 | sell_count = 600000 112 | sell_amount = sell_count * oneE18 ## 1e18 113 | 114 | tx = pricer.findOptimalSwap(token, wbtc.address, sell_amount) 115 | assert tx[1][0] == 4 ## UNIV3WITHWETH 116 | quote = tx[1][1] 117 | 118 | tx2 = pricer_legacy.findOptimalSwap(token, wbtc.address, sell_amount) 119 | assert tx2.return_value[0] == 4 ## UNIV3WITHWETH 120 | quote_legacy = tx2.return_value[1] 121 | 122 | 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 123 | assert tx[0] > 0 and tx[0] < tx2.gas_used 124 | 125 | def test_pricing_equivalency_almost_everything(oneE18, wbtc, weth, pricerwrapper, pricer_legacy): 126 | pricer = pricerwrapper 127 | token = weth # some swap (WETH-WBTC) almost in every DEX, the most gas-consuming scenario 128 | ## 1e18 129 | sell_count = 10 130 | sell_amount = sell_count * oneE18 ## 1e18 131 | 132 | tx = pricer.findOptimalSwap(token, wbtc.address, sell_amount) 133 | assert (tx[1][0] <= 3 or tx[1][0] == 5) ## CURVE or UNIV2 or SUSHI or UNIV3 or BALANCER 134 | quote = tx[1][1] 135 | 136 | tx2 = pricer_legacy.findOptimalSwap(token, wbtc.address, sell_amount) 137 | assert (tx2.return_value[0] <= 3 or tx2.return_value[0] == 5) ## CURVE or UNIV2 or SUSHI or UNIV3 or BALANCER 138 | quote_legacy = tx2.return_value[1] 139 | 140 | assert tx2.return_value[0] == tx[1][0] 141 | 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 142 | assert tx[0] > 0 and tx[0] < tx2.gas_used 143 | 144 | 145 | ### Test specific pricing functions for different underlying protocols 146 | 147 | def test_balancer_pricing_equivalency(oneE18, weth, usdc, pricer, pricer_legacy): 148 | ## 1e18 149 | sell_amount = 1 * oneE18 150 | 151 | quote = pricer.getBalancerPriceAnalytically(weth.address, sell_amount, usdc.address) 152 | quote_legacy = pricer_legacy.getBalancerPrice(weth.address, sell_amount, usdc.address).return_value 153 | 154 | assert quote >= quote_legacy # Optimized quote must be the same or better 155 | 156 | def test_balancer_pricing_with_connector_equivalency(wbtc, usdc, weth, pricer, pricer_legacy): 157 | ## 1e8 158 | sell_count = 10 159 | sell_amount = sell_count * 100000000 160 | 161 | quote = pricer.getBalancerPriceWithConnectorAnalytically(wbtc.address, sell_amount, usdc.address, weth.address) 162 | quote_legacy = pricer_legacy.getBalancerPriceWithConnector( 163 | wbtc.address, 164 | sell_amount, 165 | usdc.address, 166 | weth.address 167 | ).return_value 168 | 169 | assert quote >= quote_legacy # Optimized quote must be the same or better -------------------------------------------------------------------------------- /contracts/BalancerSwapSimulator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.7.6; 3 | pragma abicoder v2; 4 | 5 | import "./libraries/balancer/BalancerFixedPoint.sol"; 6 | import "./libraries/balancer/BalancerStableMath.sol"; 7 | 8 | struct ExactInQueryParam{ 9 | address tokenIn; 10 | address tokenOut; 11 | uint256 balanceIn; 12 | uint256 weightIn; 13 | uint256 balanceOut; 14 | uint256 weightOut; 15 | uint256 amountIn; 16 | uint256 swapFeePercentage; 17 | } 18 | 19 | struct ExactInStableQueryParam{ 20 | address[] tokens; 21 | uint256[] balances; 22 | uint256 currentAmp; 23 | uint256 tokenIndexIn; 24 | uint256 tokenIndexOut; 25 | uint256 amountIn; 26 | uint256 swapFeePercentage; 27 | } 28 | 29 | interface IERC20Metadata { 30 | function decimals() external view returns (uint8); 31 | } 32 | 33 | /// @dev Swap Simulator for Balancer V2 34 | contract BalancerSwapSimulator { 35 | uint256 internal constant _MAX_IN_RATIO = 0.3e18; 36 | 37 | /// @dev reference https://github.com/balancer-labs/balancer-v2-monorepo/blob/master/pkg/pool-weighted/contracts/WeightedMath.sol#L78 38 | function calcOutGivenIn(ExactInQueryParam memory _query) public view returns (uint256) { 39 | /********************************************************************************************** 40 | // outGivenIn // 41 | // aO = amountOut // 42 | // bO = balanceOut // 43 | // bI = balanceIn / / bI \ (wI / wO) \ // 44 | // aI = amountIn aO = bO * | 1 - | -------------------------- | ^ | // 45 | // wI = weightIn \ \ ( bI + aI ) / / // 46 | // wO = weightOut // 47 | **********************************************************************************************/ 48 | 49 | // upscale all balances and amounts 50 | _query.amountIn = _subtractSwapFeeAmount(_query.amountIn, _query.swapFeePercentage); 51 | 52 | uint256 _scalingFactorIn = _computeScalingFactorWeightedPool(_query.tokenIn); 53 | _query.amountIn = BalancerMath.mul(_query.amountIn, _scalingFactorIn); 54 | _query.balanceIn = BalancerMath.mul(_query.balanceIn, _scalingFactorIn); 55 | require(_query.balanceIn > _query.amountIn, "!amtIn"); 56 | 57 | uint256 _scalingFactorOut = _computeScalingFactorWeightedPool(_query.tokenOut); 58 | _query.balanceOut = BalancerMath.mul(_query.balanceOut, _scalingFactorOut); 59 | 60 | require(_query.amountIn <= BalancerFixedPoint.mulDown(_query.balanceIn, _MAX_IN_RATIO), "!maxIn"); 61 | 62 | uint256 denominator = BalancerFixedPoint.add(_query.balanceIn, _query.amountIn); 63 | uint256 base = BalancerFixedPoint.divUp(_query.balanceIn, denominator); 64 | uint256 exponent = BalancerFixedPoint.divDown(_query.weightIn, _query.weightOut); 65 | uint256 power = BalancerFixedPoint.powUp(base, exponent); 66 | 67 | uint256 _scaledOut = BalancerFixedPoint.mulDown(_query.balanceOut, BalancerFixedPoint.complement(power)); 68 | return BalancerMath.divDown(_scaledOut, _scalingFactorOut); 69 | } 70 | 71 | /// @dev reference https://etherscan.io/address/0x7b50775383d3d6f0215a8f290f2c9e2eebbeceb2#code#F1#L244 72 | function calcOutGivenInForStable(ExactInStableQueryParam memory _query) public view returns (uint256) { 73 | /************************************************************************************************************** 74 | // outGivenIn token x for y - polynomial equation to solve // 75 | // ay = amount out to calculate // 76 | // by = balance token out // 77 | // y = by - ay (finalBalanceOut) // 78 | // D = invariant D D^(n+1) // 79 | // A = amplification coefficient y^2 + ( S - ---------- - D) * y - ------------- = 0 // 80 | // n = number of tokens (A * n^n) A * n^2n * P // 81 | // S = sum of final balances but y // 82 | // P = product of final balances but y // 83 | **************************************************************************************************************/ 84 | 85 | // upscale all balances and amounts 86 | uint256 _tkLen = _query.tokens.length; 87 | uint256[] memory _scalingFactors = new uint256[](_tkLen); 88 | for (uint256 i = 0;i < _tkLen;++i){ 89 | _scalingFactors[i] = _computeScalingFactor(_query.tokens[i]); 90 | } 91 | 92 | _query.amountIn = _subtractSwapFeeAmount(_query.amountIn, _query.swapFeePercentage); 93 | _query.balances = _upscaleStableArray(_query.balances, _scalingFactors); 94 | _query.amountIn = _upscaleStable(_query.amountIn, _scalingFactors[_query.tokenIndexIn]); 95 | 96 | uint256 invariant = BalancerStableMath._calculateInvariant(_query.currentAmp, _query.balances, true); 97 | 98 | _query.balances[_query.tokenIndexIn] = BalancerFixedPoint.add(_query.balances[_query.tokenIndexIn], _query.amountIn); 99 | uint256 finalBalanceOut = BalancerStableMath._getTokenBalanceGivenInvariantAndAllOtherBalances(_query.currentAmp, _query.balances, invariant, _query.tokenIndexOut); 100 | 101 | uint256 _scaledOut = BalancerFixedPoint.sub(_query.balances[_query.tokenIndexOut], BalancerFixedPoint.add(finalBalanceOut, 1)); 102 | return _downscaleStable(_scaledOut, _scalingFactors[_query.tokenIndexOut]); 103 | } 104 | 105 | /// @dev scaling factors for weighted pool: reference https://etherscan.io/address/0xc45d42f801105e861e86658648e3678ad7aa70f9#code#F24#L474 106 | function _computeScalingFactorWeightedPool(address token) private view returns (uint256) { 107 | return 10**BalancerFixedPoint.sub(18, IERC20Metadata(token).decimals()); 108 | } 109 | 110 | /// @dev scaling factors for stable pool: reference https://etherscan.io/address/0x06df3b2bbb68adc8b0e302443692037ed9f91b42#code#F12#L510 111 | function _computeScalingFactor(address token) internal view returns (uint256) { 112 | return BalancerFixedPoint.ONE * 10**BalancerFixedPoint.sub(18, IERC20Metadata(token).decimals()); 113 | } 114 | 115 | function _upscaleStableArray(uint256[] memory amounts, uint256[] memory scalingFactors) internal pure returns (uint256[] memory) { 116 | uint256 _len = amounts.length; 117 | for (uint256 i = 0; i < _len;++i) { 118 | amounts[i] = _upscaleStable(amounts[i], scalingFactors[i]); 119 | } 120 | return amounts; 121 | } 122 | 123 | function _upscaleStable(uint256 amount, uint256 scalingFactor) internal pure returns (uint256) { 124 | return BalancerFixedPoint.mulDown(amount, scalingFactor); 125 | } 126 | 127 | function _downscaleStable(uint256 amount, uint256 scalingFactor) internal pure returns (uint256) { 128 | return BalancerFixedPoint.divDown(amount, scalingFactor); 129 | } 130 | 131 | function _subtractSwapFeeAmount(uint256 amount, uint256 _swapFeePercentage) public view returns (uint256) { 132 | uint256 feeAmount = BalancerFixedPoint.mulUp(amount, _swapFeePercentage); 133 | return BalancerFixedPoint.sub(amount, feeAmount); 134 | } 135 | 136 | } -------------------------------------------------------------------------------- /scripts/send_order.py: -------------------------------------------------------------------------------- 1 | from time import time 2 | import requests 3 | from brownie import * 4 | import click 5 | from rich.console import Console 6 | from dotmap import DotMap 7 | 8 | console = Console() 9 | 10 | DEV_MULTI = "0xB65cef03b9B89f99517643226d76e286ee999e77" 11 | WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" 12 | USDC = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" 13 | 14 | SLIPPAGE = 0.98 ## 2% 15 | 16 | def main(): 17 | """ 18 | DEMO ORDER 19 | Customize to use it for real life usage 20 | """ 21 | dev = connect_account() 22 | 23 | seller = CowSwapDemoSeller.at("0x75547825A99283379e0E812B7c10F832813326d6") 24 | 25 | usdc = interface.ERC20(USDC) 26 | weth = interface.ERC20(WETH) 27 | 28 | amount = usdc.balanceOf(seller) 29 | 30 | order_data = get_cowswap_order(seller, usdc, weth, amount) 31 | 32 | data = order_data.order_data 33 | uid = order_data.order_uid 34 | 35 | seller.initiateCowswapOrder(data, uid, {"from": dev}) 36 | 37 | 38 | def get_cowswap_order(contract, sell_token, buy_token, amount_in): 39 | """ 40 | Get quote, place order and return orderData as well as orderUid 41 | """ 42 | amount = amount_in 43 | 44 | # get the fee + the buy amount after fee 45 | ## TODO: Refactor to new, better endpoint: https://discord.com/channels/869166959739170836/935460632818516048/953702376345309254 46 | fee_and_quote = "https://api.cow.fi/mainnet/api/v1/feeAndQuote/sell" 47 | get_params = { 48 | "sellToken": sell_token.address, 49 | "buyToken": buy_token.address, 50 | "sellAmountBeforeFee": amount_in 51 | } 52 | r = requests.get(fee_and_quote, params=get_params) 53 | assert r.ok and r.status_code == 200 54 | 55 | # These two values are needed to create an order 56 | fee_amount = int(r.json()['fee']['amount']) 57 | buy_amount_after_fee = int(r.json()['buyAmountAfterFee']) 58 | assert fee_amount > 0 59 | assert buy_amount_after_fee > 0 60 | 61 | deadline = chain.time() + 60*60*1 # 1 hour 62 | 63 | # Submit order 64 | order_payload = { 65 | "sellToken": sell_token.address, 66 | "buyToken": buy_token.address, 67 | "sellAmount": str(amount-fee_amount), # amount that we have minus the fee we have to pay 68 | "buyAmount": str(buy_amount_after_fee), # buy amount fetched from the previous call 69 | "validTo": deadline, 70 | "appData": "0x2B8694ED30082129598720860E8E972F07AA10D9B81CAE16CA0E2CFB24743E24", # maps to https://bafybeiblq2ko2maieeuvtbzaqyhi5fzpa6vbbwnydsxbnsqoft5si5b6eq.ipfs.dweb.link 71 | "feeAmount": str(fee_amount), 72 | "kind": "sell", 73 | "partiallyFillable": False, 74 | "receiver": contract.address, 75 | "signature": contract.address, 76 | "from": contract.address, 77 | "sellTokenBalance": "erc20", 78 | "buyTokenBalance": "erc20", 79 | "signingScheme": "presign" # Very important. this tells the api you are going to sign on chain 80 | } 81 | orders_url = f"https://api.cow.fi/mainnet/api/v1/orders" 82 | r = requests.post(orders_url, json=order_payload) 83 | assert r.ok and r.status_code == 201 84 | order_uid = r.json() 85 | print(f"Payload: {order_payload}") 86 | print(f"Order uid: {order_uid}") 87 | 88 | order_data = [ 89 | sell_token.address, 90 | buy_token.address, 91 | contract.address, 92 | amount-fee_amount, 93 | buy_amount_after_fee, 94 | deadline, 95 | "0x2B8694ED30082129598720860E8E972F07AA10D9B81CAE16CA0E2CFB24743E24", 96 | fee_amount, 97 | contract.KIND_SELL(), 98 | False, 99 | contract.BALANCE_ERC20(), 100 | contract.BALANCE_ERC20() 101 | ] 102 | 103 | return DotMap( 104 | order_data=order_data, 105 | order_uid=order_uid, 106 | sellToken=sell_token.address, 107 | buyToken=buy_token.address, 108 | receiver=contract.address, 109 | sellAmount=amount-fee_amount, 110 | buyAmount=buy_amount_after_fee, 111 | validTo=deadline, 112 | appData="0x2B8694ED30082129598720860E8E972F07AA10D9B81CAE16CA0E2CFB24743E24", 113 | feeAmount=fee_amount, 114 | kind=contract.KIND_SELL(), 115 | partiallyFillable=False, 116 | sellTokenBalance=contract.BALANCE_ERC20(), 117 | buyTokenBalance=contract.BALANCE_ERC20() 118 | ) 119 | 120 | 121 | def cowswap_sell_demo(contract, sell_token, buy_token, amount_in): 122 | """ 123 | Demo of placing order and verifying it 124 | """ 125 | amount = amount_in 126 | 127 | # get the fee + the buy amount after fee 128 | ## TODO: Refactor to new, better endpoint: https://discord.com/channels/869166959739170836/935460632818516048/953702376345309254 129 | fee_and_quote = "https://api.cow.fi/mainnet/api/v1/feeAndQuote/sell" 130 | get_params = { 131 | "sellToken": sell_token.address, 132 | "buyToken": buy_token.address, 133 | "sellAmountBeforeFee": amount 134 | } 135 | r = requests.get(fee_and_quote, params=get_params) 136 | assert r.ok and r.status_code == 200 137 | 138 | # These two values are needed to create an order 139 | fee_amount = int(r.json()['fee']['amount']) 140 | buy_amount_after_fee = int(r.json()['buyAmountAfterFee']) 141 | assert fee_amount > 0 142 | assert buy_amount_after_fee > 0 143 | 144 | # Pretty random order deadline :shrug: 145 | deadline = chain.time() + 60*60*1 # 1 hour 146 | 147 | # Submit order 148 | order_payload = { 149 | "sellToken": sell_token.address, 150 | "buyToken": buy_token.address, 151 | "sellAmount": str(amount-fee_amount), # amount that we have minus the fee we have to pay 152 | "buyAmount": str(buy_amount_after_fee * SLIPPAGE), # buy amount fetched from the previous call 153 | "validTo": deadline, 154 | "appData": "0x2B8694ED30082129598720860E8E972F07AA10D9B81CAE16CA0E2CFB24743E24", # maps to https://bafybeiblq2ko2maieeuvtbzaqyhi5fzpa6vbbwnydsxbnsqoft5si5b6eq.ipfs.dweb.link 155 | "feeAmount": str(fee_amount), 156 | "kind": "sell", 157 | "partiallyFillable": False, 158 | "receiver": contract.address, 159 | "signature": contract.address, 160 | "from": contract.address, 161 | "sellTokenBalance": "erc20", 162 | "buyTokenBalance": "erc20", 163 | "signingScheme": "presign" # Very important. this tells the api you are going to sign on chain 164 | } 165 | orders_url = f"https://api.cow.fi/mainnet/api/v1/orders" 166 | r = requests.post(orders_url, json=order_payload) 167 | assert r.ok and r.status_code == 201 168 | order_uid = r.json() 169 | print(f"Payload: {order_payload}") 170 | print(f"Order uid: {order_uid}") 171 | 172 | # IERC20 sellToken; 173 | # IERC20 buyToken; 174 | # address receiver; 175 | # uint256 sellAmount; 176 | # uint256 buyAmount; 177 | # uint32 validTo; 178 | # bytes32 appData; 179 | # uint256 feeAmount; 180 | # bytes32 kind; 181 | # bool partiallyFillable; 182 | # bytes32 sellTokenBalance; 183 | # bytes32 buyTokenBalance; 184 | order_data = [ 185 | sell_token.address, 186 | buy_token.address, 187 | contract.address, 188 | amount-fee_amount, 189 | buy_amount_after_fee, 190 | deadline, 191 | "0x2B8694ED30082129598720860E8E972F07AA10D9B81CAE16CA0E2CFB24743E24", 192 | fee_amount, 193 | contract.KIND_SELL(), 194 | False, 195 | contract.BALANCE_ERC20(), 196 | contract.BALANCE_ERC20() 197 | ] 198 | 199 | hashFromContract = contract.getHash(order_data, contract.domainSeparator()) 200 | print(f"Hash from Contract: {hashFromContract}") 201 | fromContract = contract.getOrderID(order_data) 202 | print(f"Order uid from Contract: {fromContract}") 203 | 204 | contract.checkCowswapOrder(order_data, order_uid) 205 | return order_uid 206 | 207 | ## TODO Refactor to return hash map with all the fields 208 | 209 | 210 | def connect_account(): 211 | click.echo(f"You are using the '{network.show_active()}' network") 212 | dev = accounts.load(click.prompt("Account", type=click.Choice(accounts.load()))) 213 | click.echo(f"You are using: 'dev' [{dev.address}]") 214 | return dev 215 | -------------------------------------------------------------------------------- /tests/on_chain_pricer/test_balancer_pricer.py: -------------------------------------------------------------------------------- 1 | import brownie 2 | from brownie import * 3 | 4 | #import sys 5 | #from scripts.get_price import get_coingecko_price, get_coinmarketcap_price, get_coinmarketcap_metadata 6 | 7 | import pytest 8 | 9 | """ 10 | getBalancerPriceAnalytically quote for token A swapped to token B directly using given balancer pool: A - > B 11 | """ 12 | def test_get_balancer_price_stable_analytical(oneE18, usdc, dai, pricer): 13 | ## 1e18 14 | sell_count = 50000 15 | sell_amount = sell_count * oneE18 16 | 17 | ## minimum quote for DAI in USDC(1e6) 18 | p = sell_count * 0.999 * 1000000 19 | 20 | ## there is a proper pool in Balancer for DAI in USDC 21 | poolId = pricer.BALANCERV2_DAI_USDC_USDT_POOLID() 22 | quote = pricer.getBalancerQuoteWithinPoolAnalytcially(poolId, dai.address, sell_amount, usdc.address) 23 | assert quote >= p 24 | 25 | """ 26 | getBalancerPriceAnalytically quote for token A swapped to token B directly using given balancer pool: A - > B 27 | """ 28 | def test_get_balancer_price(oneE18, weth, usdc, pricer): 29 | ## 1e18 30 | sell_amount = 1 * oneE18 31 | 32 | ## minimum quote for ETH in USDC(1e6) 33 | p = 1 * 500 * 1000000 34 | 35 | quote = pricer.getBalancerPriceAnalytically(weth.address, sell_amount, usdc.address) 36 | assert quote >= p 37 | 38 | ## price sanity check with fine liquidity 39 | #p1 = get_coingecko_price('ethereum') 40 | #p2 = get_coingecko_price('usd-coin') 41 | #assert (quote / 1000000) >= (p1 / p2) * 0.98 42 | 43 | """ 44 | getBalancerPriceWithConnectorAnalytically quote for token A swapped to token B with connector token C: A -> C -> B 45 | """ 46 | def test_get_balancer_price_with_connector(oneE18, wbtc, usdc, weth, pricer): 47 | ## 1e8 48 | sell_count = 10 49 | sell_amount = sell_count * 100000000 50 | 51 | ## minimum quote for WBTC in USDC(1e6) 52 | p = sell_count * 15000 * 1000000 53 | quote = pricer.getBalancerPriceWithConnectorAnalytically(wbtc.address, sell_amount, usdc.address, weth.address) 54 | assert quote >= p 55 | 56 | quoteNotEnoughBalance = pricer.getBalancerPriceWithConnectorAnalytically(wbtc.address, sell_amount * 200, usdc.address, weth.address) 57 | assert quoteNotEnoughBalance == 0 58 | 59 | ## price sanity check with dime liquidity 60 | #yourCMCKey = 'b527d143-8597-474e-b9b2-5c28c1321c37' 61 | #p1 = get_coinmarketcap_price('3717', yourCMCKey) ## wbtc 62 | #p2 = get_coinmarketcap_price('3408', yourCMCKey) ## usdc 63 | #assert (quote / 1000000 / sell_count) >= (p1 / p2) * 0.75 64 | 65 | """ 66 | getBalancerPriceAnalytically quote for token A swapped to token B directly using given balancer pool: A - > B analytically 67 | """ 68 | def test_get_balancer_price_analytical(oneE18, weth, usdc, pricer): 69 | ## 1e18 70 | sell_amount = 1 * oneE18 71 | 72 | ## minimum quote for ETH in USDC(1e6) 73 | p = 1 * 500 * 1000000 74 | quote = pricer.getBalancerPriceAnalytically(weth.address, sell_amount, usdc.address) 75 | assert quote >= p 76 | 77 | """ 78 | getBalancerPriceAnalytically quote for token A swapped to token B directly using given balancer pool: A - > B analytically 79 | """ 80 | def test_get_balancer_price_ohm_analytical(oneE18, ohm, dai, pricer): 81 | ## 1e8 82 | sell_count = 1000 83 | sell_amount = sell_count * 1000000000 ## 1e9 84 | 85 | ## minimum quote for OHM in DAI(1e18) 86 | p = sell_count * 10 * oneE18 87 | quote = pricer.getBalancerPriceAnalytically(ohm.address, sell_amount, dai.address) 88 | assert quote >= p 89 | 90 | """ 91 | getBalancerPrice quote for token A swapped to token B directly using given balancer pool: A - > B 92 | """ 93 | def test_get_balancer_price_aurabal_analytical(oneE18, aurabal, weth, pricer): 94 | ## 1e18 95 | sell_count = 1000 96 | sell_amount = sell_count * oneE18 97 | 98 | ## minimum quote for AURABAL in WETH(1e18) 99 | p = sell_count * 0.006 * oneE18 100 | 101 | ## there is a proper pool in Balancer for AURABAL in WETH 102 | quote = pricer.getBalancerPriceAnalytically(aurabal.address, sell_amount, weth.address) 103 | assert quote >= p 104 | 105 | """ 106 | getBalancerPrice quote for token A swapped to token B directly using given balancer pool: A - > B 107 | """ 108 | def test_get_balancer_price_aurabal_bpt_analytical(oneE18, aurabal, balethbpt, pricerwrapper): 109 | pricer = pricerwrapper 110 | ## 1e18 111 | sell_count = 100 112 | sell_amount = sell_count * oneE18 113 | 114 | ## minimum quote for BAL-ETH bpt in AURABAL(1e18) https://app.balancer.fi/#/pool/0x3dd0843a028c86e0b760b1a76929d1c5ef93a2dd000200000000000000000249 115 | p = sell_count * 1 * oneE18 116 | 117 | ## there is a proper pool in Balancer for AURABAL in BAL-ETH bpt 118 | quote = pricer.findOptimalSwap(balethbpt.address, aurabal.address, sell_amount) 119 | assert quote[1][1] >= p 120 | 121 | def test_balancer_not_supported_tokens(oneE18, tusd, usdc, pricer): 122 | ## tokenIn not in the given balancer pool 123 | with brownie.reverts("!inBAL"): 124 | supported = pricer.getBalancerQuoteWithinPoolAnalytcially(pricer.BALANCERV2_DAI_USDC_USDT_POOLID(), tusd.address, 1000 * oneE18, usdc.address) 125 | ## tokenOut not in the given balancer pool 126 | with brownie.reverts("!outBAL"): 127 | supported = pricer.getBalancerQuoteWithinPoolAnalytcially(pricer.BALANCERV2_DAI_USDC_USDT_POOLID(), usdc.address, 1000 * 1000000, tusd.address) 128 | 129 | def test_get_balancer_with_connector_no_second_pair(oneE18, balethbpt, badger, weth, pricer): 130 | ## 1e18 131 | sell_amount = 1000 * oneE18 132 | 133 | ## no swap path for WETH -> BADGER in Balancer V2 134 | quoteNoPool = pricer.getBalancerPriceAnalytically(weth.address, sell_amount, badger.address) 135 | assert quoteNoPool == 0 136 | ## no swap path for BALETHBPT -> WETH -> BADGER in Balancer V2 137 | quoteBadger = pricer.getBalancerPriceWithConnectorAnalytically(balethbpt.address, sell_amount, badger.address, weth.address) 138 | assert quoteBadger == 0 139 | ## no swap path for BADGER -> WBTC -> USDI in Balancer V2 140 | quoteUSDI = pricer.getBalancerPriceWithConnectorAnalytically(badger.address, sell_amount, "0x2a54ba2964c8cd459dc568853f79813a60761b58", pricer.WBTC()) 141 | assert quoteUSDI == 0 142 | 143 | def test_get_balancer_pools(weth, usdc, wbtc, pricer): 144 | ## bveaura 145 | nonExistPool = pricer.BALANCERV2_NONEXIST_POOLID() 146 | assert pricer.getBalancerV2Pool(pricer.GRAVIAURA(), weth.address) != nonExistPool and pricer.getBalancerV2Pool(pricer.GRAVIAURA(), pricer.USDT()) == nonExistPool 147 | assert pricer.getBalancerV2Pool(pricer.AURA(), weth.address) != nonExistPool and pricer.getBalancerV2Pool(pricer.AURA(), pricer.USDT()) == nonExistPool 148 | assert pricer.getBalancerV2Pool(pricer.AURABAL(), pricer.USDT()) == nonExistPool and pricer.getBalancerV2Pool(pricer.AURABAL(), wbtc.address) == nonExistPool 149 | assert pricer.getBalancerV2Pool(pricer.COW(), weth.address) != nonExistPool and pricer.getBalancerV2Pool(pricer.COW(), usdc.address) == nonExistPool 150 | assert pricer.getBalancerV2Pool(pricer.COW(), pricer.GNO()) != nonExistPool and pricer.getBalancerV2Pool(pricer.USDT(), weth.address) == nonExistPool 151 | assert pricer.getBalancerV2Pool(pricer.OHM(), weth.address) != nonExistPool and pricer.getBalancerV2Pool(pricer.OHM(), usdc.address) == nonExistPool 152 | assert pricer.getBalancerV2Pool(pricer.AKITA(), weth.address) != nonExistPool and pricer.getBalancerV2Pool(pricer.AKITA(), usdc.address) == nonExistPool 153 | assert pricer.getBalancerV2Pool(pricer.rETH(), weth.address) != nonExistPool and pricer.getBalancerV2Pool(pricer.rETH(), pricer.USDT()) == nonExistPool 154 | assert pricer.getBalancerV2Pool(pricer.SRM(), weth.address) != nonExistPool and pricer.getBalancerV2Pool(pricer.SRM(), usdc.address) == nonExistPool 155 | assert pricer.getBalancerV2Pool(pricer.WSTETH(), weth.address) != nonExistPool and pricer.getBalancerV2Pool(pricer.WSTETH(), usdc.address) == nonExistPool 156 | assert pricer.getBalancerV2Pool(pricer.BAL(), weth.address) != nonExistPool and pricer.getBalancerV2Pool(pricer.BAL(), pricer.USDT()) == nonExistPool 157 | assert pricer.getBalancerV2Pool(pricer.GNO(), weth.address) != nonExistPool and pricer.getBalancerV2Pool(pricer.GNO(), usdc.address) == nonExistPool 158 | assert pricer.getBalancerV2Pool(pricer.FEI(), weth.address) != nonExistPool and pricer.getBalancerV2Pool(pricer.FEI(), usdc.address) == nonExistPool 159 | assert pricer.getBalancerV2Pool(pricer.CREAM(), weth.address) != nonExistPool and pricer.getBalancerV2Pool(pricer.CREAM(), usdc.address) == nonExistPool 160 | assert pricer.getBalancerV2Pool(pricer.WBTC(), pricer.BADGER()) != nonExistPool and pricer.getBalancerV2Pool(pricer.WBTC(), usdc.address) == nonExistPool 161 | assert pricer.getBalancerV2Pool(pricer.LDO(), weth.address) != nonExistPool and pricer.getBalancerV2Pool(pricer.LDO(), usdc.address) == nonExistPool and pricer.getBalancerV2Pool(pricer.LDO(), wbtc.address) == nonExistPool 162 | -------------------------------------------------------------------------------- /contracts/libraries/uniswap/TickMath.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.7.6; 3 | pragma abicoder v2; 4 | 5 | // https://github.com/Uniswap/v3-core/blob/main/contracts/libraries/TickMath.sol 6 | library TickMath { 7 | /// @dev The minimum tick that may be passed to #getSqrtRatioAtTick computed from log base 1.0001 of 2**-128 8 | int24 internal constant MIN_TICK = -887272; 9 | /// @dev The maximum tick that may be passed to #getSqrtRatioAtTick computed from log base 1.0001 of 2**128 10 | int24 internal constant MAX_TICK = -MIN_TICK; 11 | 12 | /// @dev The minimum value that can be returned from #getSqrtRatioAtTick. Equivalent to getSqrtRatioAtTick(MIN_TICK) 13 | uint160 internal constant MIN_SQRT_RATIO = 4295128739; 14 | /// @dev The maximum value that can be returned from #getSqrtRatioAtTick. Equivalent to getSqrtRatioAtTick(MAX_TICK) 15 | uint160 internal constant MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342; 16 | 17 | /// @notice Calculates sqrt(1.0001^tick) * 2^96 18 | /// @dev Throws if |tick| > max tick 19 | /// @param tick The input tick for the above formula 20 | /// @return sqrtPriceX96 A Fixed point Q64.96 number representing the sqrt of the ratio of the two assets (token1/token0) 21 | /// at the given tick 22 | function getSqrtRatioAtTick(int24 tick) internal pure returns (uint160 sqrtPriceX96) { 23 | uint256 absTick = tick < 0 ? uint256(-int256(tick)) : uint256(int256(tick)); 24 | require(absTick <= uint256(MAX_TICK), 'T'); 25 | 26 | uint256 ratio = absTick & 0x1 != 0 ? 0xfffcb933bd6fad37aa2d162d1a594001 : 0x100000000000000000000000000000000; 27 | if (absTick & 0x2 != 0) ratio = (ratio * 0xfff97272373d413259a46990580e213a) >> 128; 28 | if (absTick & 0x4 != 0) ratio = (ratio * 0xfff2e50f5f656932ef12357cf3c7fdcc) >> 128; 29 | if (absTick & 0x8 != 0) ratio = (ratio * 0xffe5caca7e10e4e61c3624eaa0941cd0) >> 128; 30 | if (absTick & 0x10 != 0) ratio = (ratio * 0xffcb9843d60f6159c9db58835c926644) >> 128; 31 | if (absTick & 0x20 != 0) ratio = (ratio * 0xff973b41fa98c081472e6896dfb254c0) >> 128; 32 | if (absTick & 0x40 != 0) ratio = (ratio * 0xff2ea16466c96a3843ec78b326b52861) >> 128; 33 | if (absTick & 0x80 != 0) ratio = (ratio * 0xfe5dee046a99a2a811c461f1969c3053) >> 128; 34 | if (absTick & 0x100 != 0) ratio = (ratio * 0xfcbe86c7900a88aedcffc83b479aa3a4) >> 128; 35 | if (absTick & 0x200 != 0) ratio = (ratio * 0xf987a7253ac413176f2b074cf7815e54) >> 128; 36 | if (absTick & 0x400 != 0) ratio = (ratio * 0xf3392b0822b70005940c7a398e4b70f3) >> 128; 37 | if (absTick & 0x800 != 0) ratio = (ratio * 0xe7159475a2c29b7443b29c7fa6e889d9) >> 128; 38 | if (absTick & 0x1000 != 0) ratio = (ratio * 0xd097f3bdfd2022b8845ad8f792aa5825) >> 128; 39 | if (absTick & 0x2000 != 0) ratio = (ratio * 0xa9f746462d870fdf8a65dc1f90e061e5) >> 128; 40 | if (absTick & 0x4000 != 0) ratio = (ratio * 0x70d869a156d2a1b890bb3df62baf32f7) >> 128; 41 | if (absTick & 0x8000 != 0) ratio = (ratio * 0x31be135f97d08fd981231505542fcfa6) >> 128; 42 | if (absTick & 0x10000 != 0) ratio = (ratio * 0x9aa508b5b7a84e1c677de54f3e99bc9) >> 128; 43 | if (absTick & 0x20000 != 0) ratio = (ratio * 0x5d6af8dedb81196699c329225ee604) >> 128; 44 | if (absTick & 0x40000 != 0) ratio = (ratio * 0x2216e584f5fa1ea926041bedfe98) >> 128; 45 | if (absTick & 0x80000 != 0) ratio = (ratio * 0x48a170391f7dc42444e8fa2) >> 128; 46 | 47 | if (tick > 0) ratio = type(uint256).max / ratio; 48 | 49 | // this divides by 1<<32 rounding up to go from a Q128.128 to a Q128.96. 50 | // we then downcast because we know the result always fits within 160 bits due to our tick input constraint 51 | // we round up in the division so getTickAtSqrtRatio of the output price is always consistent 52 | sqrtPriceX96 = uint160((ratio >> 32) + (ratio % (1 << 32) == 0 ? 0 : 1)); 53 | } 54 | 55 | /// @notice Calculates the greatest tick value such that getRatioAtTick(tick) <= ratio 56 | /// @dev Throws in case sqrtPriceX96 < MIN_SQRT_RATIO, as MIN_SQRT_RATIO is the lowest value getRatioAtTick may 57 | /// ever return. 58 | /// @param sqrtPriceX96 The sqrt ratio for which to compute the tick as a Q64.96 59 | /// @return tick The greatest tick for which the ratio is less than or equal to the input ratio 60 | function getTickAtSqrtRatio(uint160 sqrtPriceX96) internal pure returns (int24 tick) { 61 | // second inequality must be < because the price can never reach the price at the max tick 62 | require(sqrtPriceX96 >= MIN_SQRT_RATIO && sqrtPriceX96 < MAX_SQRT_RATIO, 'R'); 63 | uint256 ratio = uint256(sqrtPriceX96) << 32; 64 | 65 | uint256 r = ratio; 66 | uint256 msb = 0; 67 | 68 | assembly { 69 | let f := shl(7, gt(r, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF)) 70 | msb := or(msb, f) 71 | r := shr(f, r) 72 | } 73 | assembly { 74 | let f := shl(6, gt(r, 0xFFFFFFFFFFFFFFFF)) 75 | msb := or(msb, f) 76 | r := shr(f, r) 77 | } 78 | assembly { 79 | let f := shl(5, gt(r, 0xFFFFFFFF)) 80 | msb := or(msb, f) 81 | r := shr(f, r) 82 | } 83 | assembly { 84 | let f := shl(4, gt(r, 0xFFFF)) 85 | msb := or(msb, f) 86 | r := shr(f, r) 87 | } 88 | assembly { 89 | let f := shl(3, gt(r, 0xFF)) 90 | msb := or(msb, f) 91 | r := shr(f, r) 92 | } 93 | assembly { 94 | let f := shl(2, gt(r, 0xF)) 95 | msb := or(msb, f) 96 | r := shr(f, r) 97 | } 98 | assembly { 99 | let f := shl(1, gt(r, 0x3)) 100 | msb := or(msb, f) 101 | r := shr(f, r) 102 | } 103 | assembly { 104 | let f := gt(r, 0x1) 105 | msb := or(msb, f) 106 | } 107 | 108 | if (msb >= 128) r = ratio >> (msb - 127); 109 | else r = ratio << (127 - msb); 110 | 111 | int256 log_2 = (int256(msb) - 128) << 64; 112 | 113 | assembly { 114 | r := shr(127, mul(r, r)) 115 | let f := shr(128, r) 116 | log_2 := or(log_2, shl(63, f)) 117 | r := shr(f, r) 118 | } 119 | assembly { 120 | r := shr(127, mul(r, r)) 121 | let f := shr(128, r) 122 | log_2 := or(log_2, shl(62, f)) 123 | r := shr(f, r) 124 | } 125 | assembly { 126 | r := shr(127, mul(r, r)) 127 | let f := shr(128, r) 128 | log_2 := or(log_2, shl(61, f)) 129 | r := shr(f, r) 130 | } 131 | assembly { 132 | r := shr(127, mul(r, r)) 133 | let f := shr(128, r) 134 | log_2 := or(log_2, shl(60, f)) 135 | r := shr(f, r) 136 | } 137 | assembly { 138 | r := shr(127, mul(r, r)) 139 | let f := shr(128, r) 140 | log_2 := or(log_2, shl(59, f)) 141 | r := shr(f, r) 142 | } 143 | assembly { 144 | r := shr(127, mul(r, r)) 145 | let f := shr(128, r) 146 | log_2 := or(log_2, shl(58, f)) 147 | r := shr(f, r) 148 | } 149 | assembly { 150 | r := shr(127, mul(r, r)) 151 | let f := shr(128, r) 152 | log_2 := or(log_2, shl(57, f)) 153 | r := shr(f, r) 154 | } 155 | assembly { 156 | r := shr(127, mul(r, r)) 157 | let f := shr(128, r) 158 | log_2 := or(log_2, shl(56, f)) 159 | r := shr(f, r) 160 | } 161 | assembly { 162 | r := shr(127, mul(r, r)) 163 | let f := shr(128, r) 164 | log_2 := or(log_2, shl(55, f)) 165 | r := shr(f, r) 166 | } 167 | assembly { 168 | r := shr(127, mul(r, r)) 169 | let f := shr(128, r) 170 | log_2 := or(log_2, shl(54, f)) 171 | r := shr(f, r) 172 | } 173 | assembly { 174 | r := shr(127, mul(r, r)) 175 | let f := shr(128, r) 176 | log_2 := or(log_2, shl(53, f)) 177 | r := shr(f, r) 178 | } 179 | assembly { 180 | r := shr(127, mul(r, r)) 181 | let f := shr(128, r) 182 | log_2 := or(log_2, shl(52, f)) 183 | r := shr(f, r) 184 | } 185 | assembly { 186 | r := shr(127, mul(r, r)) 187 | let f := shr(128, r) 188 | log_2 := or(log_2, shl(51, f)) 189 | r := shr(f, r) 190 | } 191 | assembly { 192 | r := shr(127, mul(r, r)) 193 | let f := shr(128, r) 194 | log_2 := or(log_2, shl(50, f)) 195 | } 196 | 197 | int256 log_sqrt10001 = log_2 * 255738958999603826347141; // 128.128 number 198 | 199 | int24 tickLow = int24((log_sqrt10001 - 3402992956809132418596140100660247210) >> 128); 200 | int24 tickHi = int24((log_sqrt10001 + 291339464771989622907027621153398088495) >> 128); 201 | 202 | tick = tickLow == tickHi ? tickLow : getSqrtRatioAtTick(tickHi) <= sqrtPriceX96 ? tickHi : tickLow; 203 | } 204 | } -------------------------------------------------------------------------------- /contracts/UniV3SwapSimulator.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.7.6; // some underlying uniswap library require version <0.8.0 3 | pragma abicoder v2; 4 | 5 | import "./libraries/uniswap/SwapMath.sol"; 6 | import "./libraries/uniswap/TickBitmap.sol"; 7 | import "./libraries/uniswap/TickMath.sol"; 8 | import "./libraries/uniswap/FullMath.sol"; 9 | import "./libraries/uniswap/LiquidityMath.sol"; 10 | import "./libraries/uniswap/SqrtPriceMath.sol"; 11 | 12 | struct UniV3SortPoolQuery{ 13 | address _pool; 14 | address _token0; 15 | address _token1; 16 | uint24 _fee; 17 | uint256 amountIn; 18 | bool zeroForOne; 19 | } 20 | 21 | interface IERC20 { 22 | function balanceOf(address _owner) external view returns (uint256); 23 | } 24 | 25 | interface IUniswapV3PoolSwapTick { 26 | function slot0() external view returns (uint160 sqrtPriceX96, int24, uint16, uint16, uint16, uint8, bool); 27 | function liquidity() external view returns (uint128); 28 | function tickSpacing() external view returns (int24); 29 | function ticks(int24 tick) external view returns (uint128 liquidityGross, int128 liquidityNet, uint256 feeGrowthOutside0X128, uint256 feeGrowthOutside1X128, int56 tickCumulativeOutside, uint160 secondsPerLiquidityOutsideX128, uint32 secondsOutside, bool initialized); 30 | } 31 | 32 | // simplified version of https://github.com/Uniswap/v3-core/blob/main/contracts/UniswapV3Pool.sol#L561 33 | struct SwapStatus{ 34 | int256 _amountSpecifiedRemaining; 35 | uint160 _sqrtPriceX96; 36 | int24 _tick; 37 | uint128 _liquidity; 38 | int256 _amountCalculated; 39 | } 40 | 41 | /// @dev Swap Simulator for Uniswap V3 42 | contract UniV3SwapSimulator { 43 | using LowGasSafeMath for uint256; 44 | using LowGasSafeMath for int256; 45 | using SafeCast for uint256; 46 | using SafeCast for int256; 47 | 48 | /// @dev View function which aims to simplify Uniswap V3 swap logic (no oracle/fee update, etc) to 49 | /// @dev estimate the expected output for given swap parameters and slippage 50 | /// @dev simplified version of https://github.com/Uniswap/v3-core/blob/main/contracts/UniswapV3Pool.sol#L596 51 | /// @return simulated output token amount using Uniswap V3 tick-based math 52 | function simulateUniV3Swap(address _pool, address _token0, address _token1, bool _zeroForOne, uint24 _fee, uint256 _amountIn) external view returns (uint256){ 53 | // Get current state of the pool 54 | int24 _tickSpacing = IUniswapV3PoolSwapTick(_pool).tickSpacing(); 55 | // lower limit if zeroForOne in terms of slippage, or upper limit for the other direction 56 | uint160 _sqrtPriceLimitX96; 57 | // Temporary state holding key data across swap steps 58 | SwapStatus memory state; 59 | 60 | { 61 | (uint160 _currentPX96, int24 _currentTick,,,,,) = IUniswapV3PoolSwapTick(_pool).slot0(); 62 | _sqrtPriceLimitX96 = _getLimitPrice(_zeroForOne); 63 | state = SwapStatus(_amountIn.toInt256(), _currentPX96, _currentTick, IUniswapV3PoolSwapTick(_pool).liquidity(), 0); 64 | } 65 | 66 | // Loop over ticks until we exhaust all _amountIn or hit the slippage-allowed price limit 67 | while (state._amountSpecifiedRemaining != 0 && state._sqrtPriceX96 != _sqrtPriceLimitX96) { 68 | { 69 | _stepInTick(state, TickNextWithWordQuery(_pool, state._tick, _tickSpacing, _zeroForOne), _fee, _zeroForOne, _sqrtPriceLimitX96); 70 | } 71 | } 72 | 73 | return uint256(state._amountCalculated); 74 | } 75 | 76 | /// @dev allow caller to check if given amountIn would be satisfied with in-range liquidity 77 | /// @return true if in-range liquidity is good for the quote otherwise false which means a full cross-ticks simulation required 78 | function checkInRangeLiquidity(UniV3SortPoolQuery memory _sortQuery) public view returns (bool, uint256) { 79 | uint128 _liq = IUniswapV3PoolSwapTick(_sortQuery._pool).liquidity(); 80 | 81 | // are we swapping in a liquid-enough pool? 82 | if (_liq <= 0) { 83 | return (false, 0); 84 | } 85 | 86 | { 87 | (uint160 _swapAfterPrice, uint160 _tickNextPrice, uint160 _currentPriceX96) = _findSwapPriceExactIn(_sortQuery, _liq); 88 | bool _crossTick = _sortQuery.zeroForOne? (_swapAfterPrice <= _tickNextPrice) : (_swapAfterPrice >= _tickNextPrice); 89 | if (_crossTick){ 90 | return (true, 0); 91 | } else{ 92 | return (false, _getAmountOutputDelta(_swapAfterPrice, _currentPriceX96, _liq, _sortQuery.zeroForOne)); 93 | } 94 | } 95 | } 96 | 97 | /// @dev retrieve next initialized tick for given Uniswap V3 pool 98 | function _getNextInitializedTick(TickNextWithWordQuery memory _nextTickQuery) internal view returns (int24, bool, uint160) { 99 | (int24 tickNext, bool initialized) = TickBitmap.nextInitializedTickWithinOneWord(_nextTickQuery); 100 | if (tickNext < TickMath.MIN_TICK) { 101 | tickNext = TickMath.MIN_TICK; 102 | } else if (tickNext > TickMath.MAX_TICK) { 103 | tickNext = TickMath.MAX_TICK; 104 | } 105 | uint160 sqrtPriceNextX96 = TickMath.getSqrtRatioAtTick(tickNext); 106 | return (tickNext, initialized, sqrtPriceNextX96); 107 | } 108 | 109 | /// @dev return calculated output amount in the Uniswap V3 pool for given price pair 110 | /// @dev works for any swap that does not push the calculated next price past the price of the next initialized tick 111 | /// @dev check SwapMath for details 112 | function _getAmountOutputDelta(uint160 _nextPrice, uint160 _currentPrice, uint128 _liquidity, bool _zeroForOne) internal pure returns (uint256) { 113 | return _zeroForOne? SqrtPriceMath.getAmount1Delta(_nextPrice, _currentPrice, _liquidity, false) : SqrtPriceMath.getAmount0Delta(_currentPrice, _nextPrice, _liquidity, false); 114 | } 115 | 116 | /// @dev swap step in the tick 117 | function _stepInTick(SwapStatus memory state, TickNextWithWordQuery memory _nextTickQuery, uint24 _fee, bool _zeroForOne, uint160 _sqrtPriceLimitX96) view internal{ 118 | 119 | /// Fetch NEXT-STEP tick to prepare for crossing 120 | (int24 tickNext, bool initialized, uint160 sqrtPriceNextX96) = _getNextInitializedTick(_nextTickQuery); 121 | uint160 sqrtPriceStartX96 = state._sqrtPriceX96; 122 | uint160 _targetPX96 = _getTargetPriceForSwapStep(_zeroForOne, sqrtPriceNextX96, _sqrtPriceLimitX96); 123 | 124 | /// Trying to perform in-tick swap 125 | { 126 | _swapCalculation(state, _targetPX96, _fee); 127 | } 128 | 129 | /// Check if we have to cross ticks for NEXT-STEP 130 | if (state._sqrtPriceX96 == sqrtPriceNextX96) { 131 | // if the tick is initialized, run the tick transition 132 | if (initialized) { 133 | (,int128 liquidityNet,,,,,,) = IUniswapV3PoolSwapTick(_nextTickQuery.pool).ticks(tickNext); 134 | // if we're moving leftward, we interpret liquidityNet as the opposite sign safe because liquidityNet cannot be type(int128).min 135 | if (_zeroForOne) liquidityNet = -liquidityNet; 136 | state._liquidity = LiquidityMath.addDelta(state._liquidity, liquidityNet); 137 | } 138 | state._tick = _zeroForOne ? tickNext - 1 : tickNext; 139 | } else if (state._sqrtPriceX96 != sqrtPriceStartX96) { 140 | // recompute unless we're on a lower tick boundary (i.e. already transitioned ticks), and haven't moved 141 | state._tick = TickMath.getTickAtSqrtRatio(state._sqrtPriceX96); 142 | } 143 | } 144 | 145 | function _findSwapPriceExactIn(UniV3SortPoolQuery memory _sortQuery, uint128 _liq) internal view returns (uint160, uint160, uint160) { 146 | uint160 _tickNextPrice; 147 | uint160 _swapAfterPrice; 148 | (uint160 _currentPriceX96, int24 _tick,,,,,) = IUniswapV3PoolSwapTick(_sortQuery._pool).slot0(); 149 | 150 | { 151 | TickNextWithWordQuery memory _nextTickQ = TickNextWithWordQuery(_sortQuery._pool, _tick, IUniswapV3PoolSwapTick(_sortQuery._pool).tickSpacing(), _sortQuery.zeroForOne); 152 | (,,uint160 _nxtTkP) = _getNextInitializedTick(_nextTickQ); 153 | _tickNextPrice = _nxtTkP; 154 | } 155 | 156 | { 157 | uint160 _targetPX96 = _getTargetPriceForSwapStep(_sortQuery.zeroForOne, _tickNextPrice, _getLimitPrice(_sortQuery.zeroForOne)); 158 | SwapExactInParam memory _exactInParams = SwapExactInParam(_sortQuery.amountIn, _sortQuery._fee, _currentPriceX96, _targetPX96, _liq, _sortQuery.zeroForOne); 159 | (uint256 _amtIn, uint160 _newPrice) = SwapMath._getExactInNextPrice(_exactInParams); 160 | _swapAfterPrice = _newPrice; 161 | } 162 | 163 | return (_swapAfterPrice, _tickNextPrice, _currentPriceX96); 164 | } 165 | 166 | /// @dev https://etherscan.io/address/0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6#code#F1#L95 167 | function _getLimitPrice(bool _zeroForOne) internal pure returns (uint160) { 168 | return _zeroForOne? TickMath.MIN_SQRT_RATIO + 1 : TickMath.MAX_SQRT_RATIO - 1; 169 | } 170 | 171 | function _getTargetPriceForSwapStep(bool _zeroForOne, uint160 sqrtPriceNextX96, uint160 _sqrtPriceLimitX96) internal pure returns (uint160) { 172 | return (_zeroForOne ? sqrtPriceNextX96 < _sqrtPriceLimitX96 : sqrtPriceNextX96 > _sqrtPriceLimitX96)? _sqrtPriceLimitX96 : sqrtPriceNextX96; 173 | } 174 | 175 | function _swapCalculation(SwapStatus memory state, uint160 _targetPX96, uint24 _fee) internal view { 176 | (uint160 sqrtPriceX96, uint256 amountIn, uint256 amountOut, uint256 feeAmount) = SwapMath.computeSwapStep(state._sqrtPriceX96, _targetPX96, state._liquidity, state._amountSpecifiedRemaining, _fee); 177 | 178 | /// Update amounts for swap pair tokens 179 | state._sqrtPriceX96 = sqrtPriceX96; 180 | state._amountSpecifiedRemaining -= (amountIn + feeAmount).toInt256(); 181 | state._amountCalculated = state._amountCalculated.add(amountOut.toInt256()); 182 | } 183 | 184 | } --------------------------------------------------------------------------------