Skip to content

Commit

Permalink
Merge pull request #1 from Badger-Finance/feat-v4
Browse files Browse the repository at this point in the history
Feat v4
  • Loading branch information
GalloDaSballo authored Sep 14, 2022
2 parents 42f5b6e + 796c05c commit b280f3e
Show file tree
Hide file tree
Showing 16 changed files with 743 additions and 116 deletions.
62 changes: 55 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
# Fair Selling

## TODO: Readme for V4
## WARNING

⚠️⚠️⚠️ V4 functions do not revert on error, they return 0 ⚠️⚠️⚠️

A 0 may mean the function call will revert or that there is no liquidity available

For all intents and purposes, check if a quote returns 0 and if it does, do not execute it or expect a revert


-----

A [BadgerDAO](https://app.badger.com/) sponsored repo of Open Source Contracts for:

A [BadgerDAO](https://app.badger.com/) sponsored repo of Open Source Contracts for:、
- Calculating onChain Prices
- Executing the best onChain Swap

Expand All @@ -18,7 +28,7 @@ In exploring this issue we aim to:
- Can we create a "trustless swap" that is provably not frontrun nor manipulated?
- How would such a "self-defending" contract act and how would it be able to defend itself, get the best quote, and be certain of it (with statistical certainty)

## Current Release V0.3 - Pricer
## Current Release V0.4 - Pricer

# Notable Contracts

Expand All @@ -38,10 +48,19 @@ Given a tokenIn, tokenOut and AmountIn, returns a Quote from the most popular de

Covering >80% TVL on Mainnet. (Prob even more)

### New with V4

V4 adds support for Chainlink Price Feeds, all feeds are supported via the Feeds Registry

Because V4 marks the separation between "executable prices" and "ideal prices" we added new functions.

Read below for full details

## Example Usage

BREAKING CHANGE: V3 is back to `view` even for Balancer and UniV3 functions
V4 functions are `view`

!!! V4 functions do not revert on error, they return 0 !!!
### isPairSupported

Returns true if the pricer will return a non-zero quote
Expand All @@ -60,8 +79,9 @@ quote = pricer.isPairSupported(t_in, t_out, amt_in)

### findOptimalSwap

Returns the best quote given the various Dexes, used Heuristics to save gas (V0.3 will focus on this)
NOTE: While the function says optimal, this is not optimal, just best of the bunch, optimality may never be achieved fully on-chain
Finds the best quote available between the sources
Prioritizes price feeds


```solidity
function findOptimalSwap(address tokenIn, address tokenOut, uint256 amountIn) external virtual returns (Quote memory)
Expand All @@ -72,10 +92,38 @@ In Brownie
quote = pricer.findOptimalSwap(t_in, t_out, amt_in)
```

### findExecutableSwap

Finds the best executable quote
Uses PriceFeeds (if available) to verify the quote is better than the feed

```solidity
function findExecutableSwap(address tokenIn, address tokenOut, uint256 amountIn) external virtual returns (Quote memory)
```

In Brownie
```python
quote = pricer.findExecutableSwap(t_in, t_out, amt_in)
```


### unsafeFindExecutableSwap

Finds the best executable quote
Doesn't check price feeds, use at your own risk

```solidity
function unsafeFindExecutableSwap(address tokenIn, address tokenOut, uint256 amountIn) external virtual returns (Quote memory)
```

In Brownie
```python
quote = pricer.unsafeFindExecutableSwap(t_in, t_out, amt_in)
```

# Mainnet Pricing Lenient

Variation of Pricer with a slippage tollerance
Variation of Pricer with a slippage tollerance, mostly used to allow a multisig enough wiggle room for operation



Expand Down
2 changes: 2 additions & 0 deletions brownie-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ autofetch_sources: True
# require OpenZepplin Contracts
dependencies:
- OpenZeppelin/openzeppelin-contracts@4.5.0
- smartcontractkit/chainlink-brownie-contracts@0.4.2

# path remapping to support imports from GitHub/NPM
compiler:
solc:
# version: 0.8.10
remappings:
- "@oz=OpenZeppelin/openzeppelin-contracts@4.5.0/contracts/"
- "@chainlink=smartcontractkit/chainlink-brownie-contracts@0.4.2/contracts/"

reports:
exclude_contracts:
Expand Down
367 changes: 341 additions & 26 deletions contracts/OnChainPricingMainnet.sol

Large diffs are not rendered by default.

39 changes: 25 additions & 14 deletions contracts/OnChainPricingMainnetLenient.sol
Original file line number Diff line number Diff line change
@@ -1,18 +1,8 @@
// SPDX-License-Identifier: GPL-2.0
pragma solidity 0.8.10;


import {IERC20} from "@oz/token/ERC20/IERC20.sol";
import {SafeERC20} from "@oz/token/ERC20/utils/SafeERC20.sol";


import "../interfaces/uniswap/IUniswapRouterV2.sol";
import "../interfaces/curve/ICurveRouter.sol";

import {OnChainPricingMainnet} from "./OnChainPricingMainnet.sol";



/// @title OnChainPricing
/// @author Alex the Entreprenerd @ BadgerDAO
/// @dev Mainnet Version of Price Quoter, hardcoded for more efficiency
Expand All @@ -31,6 +21,8 @@ contract OnChainPricingMainnetLenient is OnChainPricingMainnet {
uint256 private constant MAX_SLIPPAGE = 500; // 5%

uint256 public slippage = 200; // 2% Initially
uint256 private constant SECONDS_PER_HOUR = 3600;
uint256 private constant SECONDS_PER_DAY = 86400;

constructor(
address _uniV3Simulator,
Expand All @@ -47,9 +39,28 @@ contract OnChainPricingMainnetLenient is OnChainPricingMainnet {

// === PRICING === //

/// @dev View function for testing the routing of the strategy
function findOptimalSwap(address tokenIn, address tokenOut, uint256 amountIn) external view override returns (Quote memory q) {
q = _findOptimalSwap(tokenIn, tokenOut, amountIn);
q.amountOut = q.amountOut * (MAX_BPS - slippage) / MAX_BPS;
/// @dev apply lenient slippage on top of parent query
function findOptimalSwap(address tokenIn, address tokenOut, uint256 amountIn) public view override returns (Quote memory q) {
q = super.findOptimalSwap(tokenIn, tokenOut, amountIn);
if (q.amountOut > 0) {
q.amountOut = q.amountOut * (MAX_BPS - slippage) / MAX_BPS;
}
}

/// @dev apply lenient slippage on top of parent query
function findExecutableSwap(address tokenIn, address tokenOut, uint256 amountIn) public view override returns (Quote memory q) {
q = super.findExecutableSwap(tokenIn, tokenOut, amountIn);
if (q.amountOut > 0) {
q.amountOut = q.amountOut * (MAX_BPS - slippage) / MAX_BPS;
}
}


/// @dev apply lenient slippage on top of parent query
function unsafeFindExecutableSwap(address tokenIn, address tokenOut, uint256 amountIn) public view override returns (Quote memory q) {
q = super.unsafeFindExecutableSwap(tokenIn, tokenOut, amountIn);
if (q.amountOut > 0) {
q.amountOut = q.amountOut * (MAX_BPS - slippage) / MAX_BPS;
}
}
}
3 changes: 2 additions & 1 deletion contracts/OnChainSwapMainnet.sol
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ enum SwapType {
UNIV3, //3
UNIV3WITHWETH, //4
BALANCER, //5
BALANCERWITHWETH //6
BALANCERWITHWETH, //6
PRICEFEED //7
}

struct Quote {
Expand Down
23 changes: 22 additions & 1 deletion contracts/tests/PricerWrapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ enum SwapType {
UNIV3, //3
UNIV3WITHWETH, //4
BALANCER, //5
BALANCERWITHWETH //6
BALANCERWITHWETH, //6
PRICEFEED //7
}

// Onchain Pricing Interface
Expand All @@ -21,8 +22,10 @@ struct Quote {
interface OnChainPricing {
function isPairSupported(address tokenIn, address tokenOut, uint256 amountIn) external view returns (bool);
function findOptimalSwap(address tokenIn, address tokenOut, uint256 amountIn) external view returns (Quote memory);
function unsafeFindExecutableSwap(address tokenIn, address tokenOut, uint256 amountIn) external view virtual returns (Quote memory q);
function checkUniV3InRangeLiquidity(address token0, address token1, uint256 amountIn, uint24 _fee, bool token0Price, address _pool) external view returns (bool, uint256);
function simulateUniV3Swap(address token0, uint256 amountIn, address token1, uint24 _fee, bool token0Price, address _pool) external view returns (uint256);
function tryQuoteWithFeed(address tokenIn, address tokenOut, uint256 amountIn) external view returns (uint256);
}
// END OnchainPricing

Expand All @@ -35,12 +38,23 @@ contract PricerWrapper {
function isPairSupported(address tokenIn, address tokenOut, uint256 amountIn) external view returns (bool) {
return OnChainPricing(pricer).isPairSupported(tokenIn, tokenOut, amountIn);
}

/// @dev mainly for gas profiling test
function findOptimalSwapNonView(address tokenIn, address tokenOut, uint256 amountIn) external returns (Quote memory) {
return OnChainPricing(pricer).findOptimalSwap(tokenIn, tokenOut, amountIn);
}

function findOptimalSwap(address tokenIn, address tokenOut, uint256 amountIn) external view returns (uint256, Quote memory) {
uint256 _gasBefore = gasleft();
Quote memory q = OnChainPricing(pricer).findOptimalSwap(tokenIn, tokenOut, amountIn);
return (_gasBefore - gasleft(), q);
}

function unsafeFindExecutableSwap(address tokenIn, address tokenOut, uint256 amountIn) external view returns (uint256, Quote memory) {
uint256 _gasBefore = gasleft();
Quote memory q = OnChainPricing(pricer).unsafeFindExecutableSwap(tokenIn, tokenOut, amountIn);
return (_gasBefore - gasleft(), q);
}

function checkUniV3InRangeLiquidity(address token0, address token1, uint256 amountIn, uint24 _fee, bool token0Price, address _pool) public view returns (uint256, bool, uint256){
uint256 _gasBefore = gasleft();
Expand All @@ -53,4 +67,11 @@ contract PricerWrapper {
uint256 _simOut = OnChainPricing(pricer).simulateUniV3Swap(token0, amountIn, token1, _fee, token0Price, _pool);
return (_gasBefore - gasleft(), _simOut);
}

/// @dev mainly for gas profiling
function tryQuoteWithFeedNonView(address tokenIn, address tokenOut, uint256 amountIn) public returns (uint256, uint256){
uint256 _gasBefore = gasleft();
uint256 _qFeed = OnChainPricing(pricer).tryQuoteWithFeed(tokenIn, tokenOut, amountIn);
return (_gasBefore - gasleft(), _qFeed);
}
}
14 changes: 8 additions & 6 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,21 +49,23 @@
## Contracts ##

@pytest.fixture
def swapexecutor():
return OnChainSwapMainnet.deploy({"from": accounts[0]})
def swapexecutor(pricer):
return OnChainSwapMainnet.deploy(pricer.address, {"from": accounts[0]})

@pytest.fixture
def pricerwrapper():
univ3simulator = UniV3SwapSimulator.deploy({"from": accounts[0]})
balancerV2Simulator = BalancerSwapSimulator.deploy({"from": accounts[0]})
pricer = OnChainPricingMainnet.deploy(univ3simulator.address, balancerV2Simulator.address, {"from": accounts[0]})
return PricerWrapper.deploy(pricer.address, {"from": accounts[0]})
c = OnChainPricingMainnet.deploy(univ3simulator.address, balancerV2Simulator.address, {"from": accounts[0]})
return PricerWrapper.deploy(c.address, {"from": accounts[0]})

@pytest.fixture
def pricer():
univ3simulator = UniV3SwapSimulator.deploy({"from": accounts[0]})
balancerV2Simulator = BalancerSwapSimulator.deploy({"from": accounts[0]})
return OnChainPricingMainnet.deploy(univ3simulator.address, balancerV2Simulator.address, {"from": accounts[0]})
c = OnChainPricingMainnetLenient.deploy(univ3simulator.address, balancerV2Simulator.address, {"from": accounts[0]})
c.setSlippage(0, {"from": accounts.at(c.TECH_OPS(), force=True)})
return c

@pytest.fixture
def pricer_legacy():
Expand Down Expand Up @@ -173,7 +175,7 @@ def aura_whale():

@pytest.fixture
def pricer_V_0_3_deployed():
return PricerWrapper.deploy("0xd27448046354839A1384D70f30e2f9528E361b03", {"from": accounts[0]})
return PricerWrapper.deploy("0x2DC7693444aCd1EcA1D6dE5B3d0d8584F3870c49", {"from": accounts[0]})

## Forces reset before each test
@pytest.fixture(autouse=True)
Expand Down
34 changes: 17 additions & 17 deletions tests/gas_benchmark/benchmark_pricer_gas.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ def test_gas_only_uniswap_v2(oneE18, weth, pricerwrapper):
token = "0xf0f9d895aca5c8678f706fb8216fa22957685a13" # some swap (CULTDAO-WETH) only in Uniswap V2
## 1e18
sell_count = 100000000
sell_amount = sell_count * 1000000000 ## 1e9
sell_amount = sell_count * oneE18 ## 1e9

tx = pricer.findOptimalSwap(token, weth.address, sell_amount)
tx = pricer.unsafeFindExecutableSwap(token, weth.address, sell_amount)
assert tx[1][0] == 1 ## UNIV2
assert tx[1][1] > 0
assert tx[0] <= 80000 ## 73925 in test simulation
assert tx[0] <= 60000

def test_gas_uniswap_v2_sushi(oneE18, weth, pricerwrapper):
pricer = pricerwrapper
Expand All @@ -27,10 +27,10 @@ def test_gas_uniswap_v2_sushi(oneE18, weth, pricerwrapper):
sell_count = 5000
sell_amount = sell_count * oneE18 ## 1e18

tx = pricer.findOptimalSwap(token, weth.address, sell_amount)
assert (tx[1][0] == 1 or tx[1][0] == 2) ## UNIV2 or SUSHI
tx = pricer.unsafeFindExecutableSwap(token, weth.address, sell_amount)
assert (tx[1][0] == 1 or tx[1][0] == 2) ## SUSHI or UNIV2
assert tx[1][1] > 0
assert tx[0] <= 90000 ## 83158 in test simulation
assert tx[0] <= 60000

def test_gas_only_balancer_v2(oneE18, weth, aura, pricerwrapper):
pricer = pricerwrapper
Expand All @@ -39,10 +39,10 @@ def test_gas_only_balancer_v2(oneE18, weth, aura, pricerwrapper):
sell_count = 8000
sell_amount = sell_count * oneE18 ## 1e18

tx = pricer.findOptimalSwap(token, weth.address, sell_amount)
tx = pricer.unsafeFindExecutableSwap(token, weth.address, sell_amount)
assert tx[1][0] == 5 ## BALANCER
assert tx[1][1] > 0
assert tx[0] <= 110000 ## 101190 in test simulation
assert tx[0] <= 70000

def test_gas_only_balancer_v2_with_weth(oneE18, wbtc, aura, pricerwrapper):
pricer = pricerwrapper
Expand All @@ -51,10 +51,10 @@ def test_gas_only_balancer_v2_with_weth(oneE18, wbtc, aura, pricerwrapper):
sell_count = 8000
sell_amount = sell_count * oneE18 ## 1e18

tx = pricer.findOptimalSwap(token, wbtc.address, sell_amount)
tx = pricer.unsafeFindExecutableSwap(token, wbtc.address, sell_amount)
assert tx[1][0] == 6 ## BALANCERWITHWETH
assert tx[1][1] > 0
assert tx[0] <= 170000 ## 161690 in test simulation
assert tx[0] <= 120000

def test_gas_only_uniswap_v3(oneE18, weth, pricerwrapper):
pricer = pricerwrapper
Expand All @@ -63,10 +63,10 @@ def test_gas_only_uniswap_v3(oneE18, weth, pricerwrapper):
sell_count = 600000
sell_amount = sell_count * oneE18 ## 1e18

tx = pricer.findOptimalSwap(token, weth.address, sell_amount)
tx = pricer.unsafeFindExecutableSwap(token, weth.address, sell_amount)
assert tx[1][0] == 3 ## UNIV3
assert tx[1][1] > 0
assert tx[0] <= 160000 ## 158204 in test simulation
assert tx[0] <= 140000

def test_gas_only_uniswap_v3_with_weth(oneE18, wbtc, pricerwrapper):
pricer = pricerwrapper
Expand All @@ -75,10 +75,10 @@ def test_gas_only_uniswap_v3_with_weth(oneE18, wbtc, pricerwrapper):
sell_count = 600000
sell_amount = sell_count * oneE18 ## 1e18

tx = pricer.findOptimalSwap(token, wbtc.address, sell_amount)
tx = pricer.unsafeFindExecutableSwap(token, wbtc.address, sell_amount)
assert tx[1][0] == 4 ## UNIV3WITHWETH
assert tx[1][1] > 0
assert tx[0] <= 230000 ## 227498 in test simulation
assert tx[0] <= 215000

def test_gas_almost_everything(oneE18, wbtc, weth, pricerwrapper):
pricer = pricerwrapper
Expand All @@ -87,8 +87,8 @@ def test_gas_almost_everything(oneE18, wbtc, weth, pricerwrapper):
sell_count = 10
sell_amount = sell_count * oneE18 ## 1e18

tx = pricer.findOptimalSwap(token, wbtc.address, sell_amount)
assert (tx[1][0] <= 3 or tx[1][0] == 5) ## CURVE or UNIV2 or SUSHI or UNIV3 or BALANCER
tx = pricer.unsafeFindExecutableSwap(token, wbtc.address, sell_amount)
assert (tx[1][0] <= 5 or tx[1][0] == 5) ##
assert tx[1][1] > 0
assert tx[0] <= 210000 ## 200229 in test simulation
assert tx[0] <= 210000

Loading

0 comments on commit b280f3e

Please sign in to comment.