Compared to the mindfuck of ticks, liquidity and square root prices from the Pool contract, and the obscure bit/byte/word manipulation of TickLens, the Quoter seems quite tame and maybe even … boring?
It’s a workhorse contract, designed to do one thing: give accurate quotes for exact input and output swaps. That’s it!
It is a sister contract to the Router, and you’ll find many of the arguments and function names very familiar.
The router is designed for gas efficiency, so it doesn’t have a lot of “extras”. The UniswapV2 Router allowed you to get amounts in and out, in addition to performing the swaps themselves. The UniswapV3 Router just does swaps, and the nearest analog to getAmountsOut lives in the Quoter.
Contract Functions
The Quoter offers four simple functions, styled after the four from the Router:
function quoteExactInput(
bytes memory path,
uint256 amountIn
) external returns (
uint256 amountOut
)function quoteExactInputSingle(
address tokenIn,
address tokenOut,
uint24 fee,
uint256 amountIn,
uint160 sqrtPriceLimitX96
) external returns (
uint256amountOut
)function quoteExactOutput(
bytes memory path,
uint256 amountOut
) external returns (
uint256 amountIn
)function quoteExactOutputSingle(
address tokenIn,
address tokenOut,
uint24 fee,
uint256 amountOut,
uint160 sqrtPriceLimitX96
) external returns (
uint256amountIn
)
Console Exploration
We will return to our familiar DAI/WETH pool at 0xC2e9F25Be6257c210d7Adf0D4Cd6E3E881ba25f8, test the various functions that the Quoter provides, and use it to learn more about how the Pool contract works with respect to swaps and prices.
To the console!
I am going to fork from Ethereum mainnet at block height 15654820. If you follow along later, you can use this same fork height to get exact matches for all examples that follow.
(.venv) devil@hades:~/bots$ BLOCK=15654820 brownie console --network mainnet-fork-atblock
Brownie v1.19.1 - Python development framework for Ethereum
BotsProject is the active project.
Launching 'ganache-cli --chain.vmErrorsOnRPCResponse true --wallet.totalAccounts 10 --hardfork istanbul --fork.url https://rpc.ankr.com/eth@15654820 --miner.blockGasLimit 12000000 --wallet.mnemonic brownie --server.port 6969'...
Brownie environment is ready.
>>> chain.height
15654821
Now, make some objects for handy access to the Quoter, Router, TickLens, our LP, and the tokens in question:
>>> quoter = Contract.from_explorer(
'0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6'
)
>>> router = Contract.from_explorer(
'0xE592427A0AEce92De3Edee1F18E0157C05861564'
)
>>> lens = Contract.from_explorer(
'0xbfd8137f7d1516D3ea5cA83523914859ec47F573'
)
>>> lp = Contract.from_explorer(
'0xC2e9F25Be6257c210d7Adf0D4Cd6E3E881ba25f8'
)
>>> weth = Contract.from_explorer(
'0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'
)
>>> dai = Contract.from_explorer(
'0x6B175474E89094C44Da98b954EedeAC495271d0F'
)
Now let’s get ourselves some fake WETH and approve it for use at the Router:
>>> weth.deposit({'from':accounts[0],'value':10*10**18})
Transaction sent: 0xab33517ae3ebb4a9c593adaa62c093f4ceb4739cc2add95768331ec5c6da5c8a
Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 6
Transaction confirmed Block: 15654822 Gas used: 43738 (0.36%)
<Transaction '0xab33517ae3ebb4a9c593adaa62c093f4ceb4739cc2add95768331ec5c6da5c8a'>
>>> weth.balanceOf(accounts[0])
10000000000000000000
>>> weth.approve(router.address,weth.balanceOf(accounts[0]),{'from':a
ccounts[0]})
Transaction sent: 0x4ff074901f98b32b1aaabc03a803df7f9115c2a49e8add769345273ead189987
Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 7
Transaction confirmed Block: 15654823 Gas used: 43952 (0.37%)
<Transaction '0x4ff074901f98b32b1aaabc03a803df7f9115c2a49e8add769345273ead189987'>
And make a snapshot in case we need to return to this state:
>>> chain.snapshot()
The first thing we’d like to verify is that the Quoter gives the correct amounts that we would get from the Router. First we will get a quote for an exact input of 10 WETH, then submit that same input to the router and check. For an exact input swap through a single pool, use the quoteExactInputSingle
function:
>>> quoter.quoteExactInputSingle(weth.address, dai.address, 3000, 10*10**
18, 0)
File "<console>", line 1, in <module>
File "brownie/network/contract.py", line 1861, in __call__
return self.transact(*args)
File "brownie/network/contract.py", line 1729, in transact
raise AttributeError(
AttributeError: Final argument must be a dict of transaction parameters that includes a `from` field specifying the sender of the transaction
WHAT? Why do I need to generate a transaction?
The tricky thing about the Quoter is that the methods are marked external
but not view
. These functions consume gas, and if you broadcast them on a live blockchain they will cost money!
The way to manage this is to use the built-in call()
method, which will simulate the transaction using eth_call
without broadcasting a transaction and burning real gas.
Do not convert this to a transaction, because Brownie (and any other web3 library) will happily broadcast these transactions and drain gas from your bot operator account.
Once more for clarity: Always use call()
for Quoter functions!
So now let’s try again with the call()
method added:
>>> quoter.quoteExactInputSingle.call(
weth.address,
dai.address,
3000,
10*10**18,
0
)
13036497268644356695772
Now it behaves the way you expect. In fact, the way this contract works is by reverting as part of normal execution, and then passing the return value that you want as the revert message! Check out the Quoter contract source if you’re interested in seeing this mechanism.
Convert the decimals to discover that the swap value is approximately 13,036 DAI:
>>> 13036497268644356695772 / 10**dai.decimals()
13036.497268644356
Now let’s do the same transaction in the Router, and see what we get:
>>> tx = router.exactInputSingle(
(
weth.address,
dai.address,
3000,
accounts[0],
99999999999999999,
10*10**18,
0,
0
),
{'from':accounts[0]}
)
Transaction sent: 0x11d02677df4edc5993fa94dd175faf9cb0bc0f9cb169af715e06cad7e648b406
Gas price: 0.0 gwei Gas limit: 12000000 Nonce: 8
Transaction confirmed Block: 15654824 Gas used: 105387 (0.88%)
>>> tx.info()
Transaction was Mined
---------------------
Tx Hash: 0x11d02677df4edc5993fa94dd175faf9cb0bc0f9cb169af715e06cad7e648b406
From: 0x66aB6D9362d4F35596279692F0251Db635165871
To: 0xE592427A0AEce92De3Edee1F18E0157C05861564
Value: 0
Function: Transaction
Block: 15654824
Gas Used: 105387 / 12000000 (0.9%)
Events In This Transaction
--------------------------
├── 0x6B175474E89094C44Da98b954EedeAC495271d0F
│ └── Transfer
│ ├── src: 0xC2e9F25Be6257c210d7Adf0D4Cd6E3E881ba25f8
│ ├── dst: 0x66aB6D9362d4F35596279692F0251Db635165871
│ └── wad: 13036497268644356695772
│
├── 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
│ └── Transfer
│ ├── src: 0x66aB6D9362d4F35596279692F0251Db635165871
│ ├── dst: 0xC2e9F25Be6257c210d7Adf0D4Cd6E3E881ba25f8
│ └── wad: 10000000000000000000
│
└── 0xC2e9F25Be6257c210d7Adf0D4Cd6E3E881ba25f8
└── Swap
├── sender: 0xE592427A0AEce92De3Edee1F18E0157C05861564
├── recipient: 0x66aB6D9362d4F35596279692F0251Db635165871
├── amount0: -13036497268644356695772
├── amount1: 10000000000000000000
├── sqrtPriceX96: 2191278529536427425406189775
├── liquidity: 1538646794036765605560899
└── tick: -71761
The transfer events show that the pool swapped my 10000000000000000000 WETH for 13036497268644356695772 DAI. This is the same value that the Quoter gave me, so we can be confident that the contracts are in agreement.
Now let’s revert the chain back to our original position and check that the balances are as before:
>>> chain.revert()
15654823
>>> dai.balanceOf(accounts[0])
0
>>> weth.balanceOf(accounts[0])
10000000000000000000
Here Comes the Curveball
Did you know there are two Quoter contracts?