Quoter Contract
To integrate our updated Pool contract into the front-end app, we need a way to calculate swap amounts without making a swap. Users will type in the amount they want to sell, and we want to calculate and show them the amount they’ll get in exchange. We’ll do this through the Quoter contract.
Since liquidity in Uniswap V3 is scattered over multiple price ranges, we cannot calculate swap amounts with a formula (which was possible in Uniswap V2). The design of Uniswap V3 forces us to use a different approach: to calculate swap amounts, we’ll initiate a real swap and will interrupt it in the callback function, grabbing the amounts calculated by the Pool contract. That is, we have to simulate a real swap to calculate the output amount!
Again, we’ll make a helper contract for that:
contract UniswapV3Quoter {
struct QuoteParams {
address pool;
uint256 amountIn;
bool zeroForOne;
}
function quote(QuoteParams memory params)
public
returns (
uint256 amountOut,
uint160 sqrtPriceX96After,
int24 tickAfter
)
{
...
Quoter is a contract that implements only one public function–quote
. Quoter is a universal contract that works with any pool so it takes pool address as a parameter. The other parameters (amountIn
and zeroForOne
) are required to simulate a swap.
try
IUniswapV3Pool(params.pool).swap(
address(this),
params.zeroForOne,
params.amountIn,
abi.encode(params.pool)
)
{} catch (bytes memory reason) {
return abi.decode(reason, (uint256, uint160, int24));
}
The only thing that the contract does is calling the swap
function of a pool. The call is expected to revert (i.e. throw an error)–we’ll do this in the swap callback. In the case of a revert, the revert reason is decoded and returned; quote
will never revert. Notice that, in the extra data, we’re passing only the pool address–in the swap callback, we’ll use it to get the pool’s slot0
after a swap.
function uniswapV3SwapCallback(
int256 amount0Delta,
int256 amount1Delta,
bytes memory data
) external view {
address pool = abi.decode(data, (address));
uint256 amountOut = amount0Delta > 0
? uint256(-amount1Delta)
: uint256(-amount0Delta);
(uint160 sqrtPriceX96After, int24 tickAfter) = IUniswapV3Pool(pool)
.slot0();
In the swap callback, we’re collecting values that we need: output amount, new price, and corresponding tick. Next, we need to save these values and revert:
assembly {
let ptr := mload(0x40)
mstore(ptr, amountOut)
mstore(add(ptr, 0x20), sqrtPriceX96After)
mstore(add(ptr, 0x40), tickAfter)
revert(ptr, 96)
}
For gas optimization, this piece is implemented in Yul, the language used for inline assembly in Solidity. Let’s break it down:
mload(0x40)
reads the pointer of the next available memory slot (memory in EVM is organized in 32-byte slots);- at that memory slot,
mstore(ptr, amountOut)
writesamountOut
; mstore(add(ptr, 0x20), sqrtPriceX96After)
writessqrtPriceX96After
right afteramountOut
;mstore(add(ptr, 0x40), tickAfter)
writestickAfter
aftersqrtPriceX96After
;revert(ptr, 96)
reverts the call and returns 96 bytes (total length of the values we wrote to memory) of data at addressptr
(start of the data we wrote above).
So, we’re concatenating the bytes representations of the values we need (exactly what abi.encode()
does). Notice that the offsets are always 32 bytes, even though sqrtPriceX96After
takes 20 bytes (uint160
) and tickAfter
takes 3 bytes (int24
). This is so we could use abi.decode()
to decode the data: its counterpart, abi.encode()
, encodes all integers as 32-byte words.
Aaaand… it’s gone done.
Recap
Let’s recap to better understand the algorithm:
quote
callsswap
of a pool with input amount and swap direction;swap
performs a real swap, it runs the loop to fill the input amount specified by the user;- to get tokens from the user,
swap
calls the swap callback on the caller; - the caller (Quote contract) implements the callback, in which it reverts with output amount, new price, and new tick;
- the revert bubbles up to the initial
quote
call; - in
quote
, the revert is caught, revert reason is decoded and returned as the result of callingquote
.
I hope this is clear!
Quoter Limitation
This design has one significant limitation: since quote
calls the swap
function of the Pool contract, and the swap
function is not a pure or view function (because it modifies contract state), quote
cannot also be pure or view. swap
modifies state and so does quote
, even if not in Quoter contract. But we treat quote
as a getter, a function that only reads contract data. This inconsistency means that EVM will use CALL opcode instead of STATICCALL when quote
is called. This is not a big problem since Quoter reverts in the swap callback, and reverting resets the state modified during a call–this guarantees that quote
won’t modify the state of the Pool contract (no actual trade will happen).
Another inconvenience that comes from this issue is that calling quote
from a client library (Ethers.js, Web3.js, etc.) will trigger a transaction. To fix this, we’ll need to force the library to make a static call. We’ll see how to do this in Ethers.js later in this milestone.