diff --git a/src/keywords.json b/src/keywords.json index 0da08e7f..8a6ef661 100644 --- a/src/keywords.json +++ b/src/keywords.json @@ -8,6 +8,15 @@ "trade", "swapping" ], + "/quoter": [ + "quoter", + "quoting", + "exact input", + "exact output", + "single", + "multi", + "multihop" + ], "/initialize": [ "pool", "initialize", diff --git a/src/pages/quoter/Quoter.sol b/src/pages/quoter/Quoter.sol new file mode 100644 index 00000000..2f9c0b19 --- /dev/null +++ b/src/pages/quoter/Quoter.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import {IHooks} from "v4-core/interfaces/IHooks.sol"; +import {Hooks} from "v4-core/libraries/Hooks.sol"; +import {TickMath} from "v4-core/libraries/TickMath.sol"; +import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol"; +import {PoolKey} from "v4-core/types/PoolKey.sol"; +import {BalanceDelta} from "v4-core/types/BalanceDelta.sol"; +import {PoolId, PoolIdLibrary} from "v4-core/types/PoolId.sol"; +import {Constants} from "v4-core/../test/utils/Constants.sol"; +import {CurrencyLibrary, Currency} from "v4-core/types/Currency.sol"; +import {HookTest} from "@v4-by-example/utils/HookTest.sol"; +import {IQuoter} from "v4-periphery/interfaces/IQuoter.sol"; +import {Quoter} from "v4-periphery/lens/Quoter.sol"; + +contract QuoterTest is HookTest { + using PoolIdLibrary for PoolKey; + using CurrencyLibrary for Currency; + + PoolKey poolKey; + PoolId poolId; + Quoter quoter; + + function setUp() public { + // creates the pool manager, test tokens, and other utility routers + HookTest.initHookTestEnv(); + quoter = new Quoter(address(manager)); + + // Create the pool + poolKey = + PoolKey(Currency.wrap(address(token0)), Currency.wrap(address(token1)), 3000, 60, IHooks(address(0x0))); + poolId = poolKey.toId(); + initializeRouter.initialize(poolKey, Constants.SQRT_RATIO_1_1, ZERO_BYTES); + + // Provide liquidity to the pool + modifyPositionRouter.modifyLiquidity( + poolKey, + IPoolManager.ModifyLiquidityParams(TickMath.minUsableTick(60), TickMath.maxUsableTick(60), 1000 ether), + ZERO_BYTES + ); + } + + function testQuoter_output() public { + uint128 amountIn = 1e18; + bool zeroForOne = true; + uint160 MAX_SLIPPAGE = zeroForOne ? MIN_PRICE_LIMIT : MAX_PRICE_LIMIT; + + // get the quote + PoolKey memory key = poolKey; + (int128[] memory deltaAmounts, uint160 sqrtPriceX96After,) = quoter.quoteExactInputSingle( + IQuoter.QuoteExactSingleParams(key, zeroForOne, address(this), amountIn, MAX_SLIPPAGE, ZERO_BYTES) + ); + + // output is amount 1 + int128 outputAmount = deltaAmounts[1]; + console2.log("Quoted output amount: ", int256(outputAmount)); + + // Perform a test swap + BalanceDelta swapDelta = swap(poolKey, int256(uint256(amountIn)), zeroForOne, ZERO_BYTES); + + // quote agrees with the actual swap + assertEq(outputAmount, swapDelta.amount1()); + } + + function testQuoter_input() public { + uint128 amountOut = 1e18; + bool zeroForOne = true; + uint160 MAX_SLIPPAGE = zeroForOne ? MIN_PRICE_LIMIT : MAX_PRICE_LIMIT; + + // get the quote + PoolKey memory key = poolKey; + (int128[] memory deltaAmounts, uint160 sqrtPriceX96After,) = quoter.quoteExactOutputSingle( + IQuoter.QuoteExactSingleParams(key, zeroForOne, address(this), amountOut, MAX_SLIPPAGE, ZERO_BYTES) + ); + + // input (quoted) is amount 0 + int128 inputAmount = deltaAmounts[0]; + console2.log("Quoted input amount: ", int256(inputAmount)); + + // Perform a exact-output swap + BalanceDelta swapDelta = swap(poolKey, -int256(uint256(amountOut)), zeroForOne, ZERO_BYTES); + assertEq(inputAmount, swapDelta.amount0()); + (uint160 sqrtPriceX96,,) = manager.getSlot0(poolId); + assertEq(sqrtPriceX96After, sqrtPriceX96); + } +} diff --git a/src/pages/quoter/QuoterSnippet.solsnippet b/src/pages/quoter/QuoterSnippet.solsnippet new file mode 100644 index 00000000..169c15d4 --- /dev/null +++ b/src/pages/quoter/QuoterSnippet.solsnippet @@ -0,0 +1,13 @@ +import {IQuoter} from "v4-periphery/interfaces/IQuoter.sol"; + +PoolKey memory key; +uint128 amountIn = 1e18; +bool zeroForOne = true; +uint160 MAX_SLIPPAGE = zeroForOne ? MIN_PRICE_LIMIT : MAX_PRICE_LIMIT; +bytes memory hookData; + +// exact input will quote deltaAmounts[1] (output) +// exact output will quote deltaAmounts[0] (input) +(int128[] memory deltaAmounts, uint160 sqrtPriceX96After,) = quoter.quoteExactInputSingle( + IQuoter.QuoteExactSingleParams(key, zeroForOne, address(this), amountIn, MAX_SLIPPAGE, hookData) +); diff --git a/src/pages/quoter/TemplateSnippet.solsnippet b/src/pages/quoter/TemplateSnippet.solsnippet deleted file mode 100644 index 194c148a..00000000 --- a/src/pages/quoter/TemplateSnippet.solsnippet +++ /dev/null @@ -1,16 +0,0 @@ -import {Hooks} from "v4-core/libraries/Hooks.sol"; - -function getHookPermissions() public pure override returns (Hooks.Permissions memory) { - return Hooks.Permissions({ - beforeInitialize: false, - afterInitialize: false, - beforeModifyPosition: true, - afterModifyPosition: true, - beforeSwap: true, - afterSwap: true, - beforeDonate: false, - afterDonate: false, - noOp: false, - accessLock: false - }); -} diff --git a/src/pages/quoter/index.html.ts b/src/pages/quoter/index.html.ts index 1f6d01d8..943fd0eb 100644 --- a/src/pages/quoter/index.html.ts +++ b/src/pages/quoter/index.html.ts @@ -1,93 +1,147 @@ // metadata export const version = "0.8.20" -export const title = "TEMPLATE" -export const description = "TEMPLATE" +export const title = "Quoter" +export const description = "Offchain Quoter, to fetch input/output amounts" export const keywords = [ - "template", - "example", + "quoter", + "quoting", + "exact input", + "exact output", + "single", + "multi", + "multihop", ] export const codes = [ { - fileName: "Template.sol", - code: "Ly8gU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IE1JVApwcmFnbWEgc29saWRpdHkgXjAuOC4yMDsKCmltcG9ydCB7QmFzZUhvb2t9IGZyb20gIkB2NC1ieS1leGFtcGxlL3V0aWxzL0Jhc2VIb29rLnNvbCI7CgppbXBvcnQge0hvb2tzfSBmcm9tICJ2NC1jb3JlL2xpYnJhcmllcy9Ib29rcy5zb2wiOwppbXBvcnQge0lQb29sTWFuYWdlcn0gZnJvbSAidjQtY29yZS9pbnRlcmZhY2VzL0lQb29sTWFuYWdlci5zb2wiOwppbXBvcnQge1Bvb2xLZXl9IGZyb20gInY0LWNvcmUvdHlwZXMvUG9vbEtleS5zb2wiOwppbXBvcnQge1Bvb2xJZCwgUG9vbElkTGlicmFyeX0gZnJvbSAidjQtY29yZS90eXBlcy9Qb29sSWQuc29sIjsKCmNvbnRyYWN0IE5vT3BTd2FwIGlzIEJhc2VIb29rIHsKICAgIHVzaW5nIFBvb2xJZExpYnJhcnkgZm9yIFBvb2xLZXk7CgogICAgbWFwcGluZyhQb29sSWQgPT4gdWludDI1NiBjb3VudCkgcHVibGljIGJlZm9yZVN3YXBDb3VudDsKCiAgICBjb25zdHJ1Y3RvcihJUG9vbE1hbmFnZXIgX3Bvb2xNYW5hZ2VyKSBCYXNlSG9vayhfcG9vbE1hbmFnZXIpIHt9CgogICAgZnVuY3Rpb24gZ2V0SG9va1Blcm1pc3Npb25zKCkgcHVibGljIHB1cmUgb3ZlcnJpZGUgcmV0dXJucyAoSG9va3MuUGVybWlzc2lvbnMgbWVtb3J5KSB7CiAgICAgICAgcmV0dXJuIEhvb2tzLlBlcm1pc3Npb25zKHsKICAgICAgICAgICAgYmVmb3JlSW5pdGlhbGl6ZTogZmFsc2UsCiAgICAgICAgICAgIGFmdGVySW5pdGlhbGl6ZTogZmFsc2UsCiAgICAgICAgICAgIGJlZm9yZU1vZGlmeVBvc2l0aW9uOiBmYWxzZSwKICAgICAgICAgICAgYWZ0ZXJNb2RpZnlQb3NpdGlvbjogZmFsc2UsCiAgICAgICAgICAgIGJlZm9yZVN3YXA6IHRydWUsIC8vIC0tIE5vLW9wJ2luZyB0aGUgc3dhcCAtLSAgLy8KICAgICAgICAgICAgYWZ0ZXJTd2FwOiBmYWxzZSwKICAgICAgICAgICAgYmVmb3JlRG9uYXRlOiBmYWxzZSwKICAgICAgICAgICAgYWZ0ZXJEb25hdGU6IGZhbHNlLAogICAgICAgICAgICBub09wOiB0cnVlLCAvLyAtLSBFTkFCTEUgTk8tT1AgLS0gIC8vCiAgICAgICAgICAgIGFjY2Vzc0xvY2s6IGZhbHNlCiAgICAgICAgfSk7CiAgICB9CgogICAgZnVuY3Rpb24gYmVmb3JlU3dhcChhZGRyZXNzLCBQb29sS2V5IGNhbGxkYXRhIGtleSwgSVBvb2xNYW5hZ2VyLlN3YXBQYXJhbXMgY2FsbGRhdGEgcGFyYW1zLCBieXRlcyBjYWxsZGF0YSkKICAgICAgICBleHRlcm5hbAogICAgICAgIG92ZXJyaWRlCiAgICAgICAgcmV0dXJucyAoYnl0ZXM0KQogICAgewogICAgICAgIC8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0gLy8KICAgICAgICAvLyBFeGFtcGxlIE5vT3A6IGlmIHN3YXAgYW1vdW50IGlzIDY5ZTE4LCB0aGVuIHRoZSBzd2FwIHdpbGwgYmUgc2tpcHBlZCAgICAgICAgICAgIC8vCiAgICAgICAgLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSAvLwogICAgICAgIGlmIChwYXJhbXMuYW1vdW50U3BlY2lmaWVkID09IDY5ZTE4KSByZXR1cm4gSG9va3MuTk9fT1BfU0VMRUNUT1I7CgogICAgICAgIGJlZm9yZVN3YXBDb3VudFtrZXkudG9JZCgpXSsrOwogICAgICAgIHJldHVybiBCYXNlSG9vay5iZWZvcmVTd2FwLnNlbGVjdG9yOwogICAgfQp9Cg==", + fileName: "Quoter.sol", + code: "Ly8gU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IE1JVApwcmFnbWEgc29saWRpdHkgXjAuOC4xOTsKCmltcG9ydCAiZm9yZ2Utc3RkL1Rlc3Quc29sIjsKaW1wb3J0IHtJSG9va3N9IGZyb20gInY0LWNvcmUvaW50ZXJmYWNlcy9JSG9va3Muc29sIjsKaW1wb3J0IHtIb29rc30gZnJvbSAidjQtY29yZS9saWJyYXJpZXMvSG9va3Muc29sIjsKaW1wb3J0IHtUaWNrTWF0aH0gZnJvbSAidjQtY29yZS9saWJyYXJpZXMvVGlja01hdGguc29sIjsKaW1wb3J0IHtJUG9vbE1hbmFnZXJ9IGZyb20gInY0LWNvcmUvaW50ZXJmYWNlcy9JUG9vbE1hbmFnZXIuc29sIjsKaW1wb3J0IHtQb29sS2V5fSBmcm9tICJ2NC1jb3JlL3R5cGVzL1Bvb2xLZXkuc29sIjsKaW1wb3J0IHtCYWxhbmNlRGVsdGF9IGZyb20gInY0LWNvcmUvdHlwZXMvQmFsYW5jZURlbHRhLnNvbCI7CmltcG9ydCB7UG9vbElkLCBQb29sSWRMaWJyYXJ5fSBmcm9tICJ2NC1jb3JlL3R5cGVzL1Bvb2xJZC5zb2wiOwppbXBvcnQge0NvbnN0YW50c30gZnJvbSAidjQtY29yZS8uLi90ZXN0L3V0aWxzL0NvbnN0YW50cy5zb2wiOwppbXBvcnQge0N1cnJlbmN5TGlicmFyeSwgQ3VycmVuY3l9IGZyb20gInY0LWNvcmUvdHlwZXMvQ3VycmVuY3kuc29sIjsKaW1wb3J0IHtIb29rVGVzdH0gZnJvbSAiQHY0LWJ5LWV4YW1wbGUvdXRpbHMvSG9va1Rlc3Quc29sIjsKaW1wb3J0IHtJUXVvdGVyfSBmcm9tICJ2NC1wZXJpcGhlcnkvaW50ZXJmYWNlcy9JUXVvdGVyLnNvbCI7CmltcG9ydCB7UXVvdGVyfSBmcm9tICJ2NC1wZXJpcGhlcnkvbGVucy9RdW90ZXIuc29sIjsKCmNvbnRyYWN0IFF1b3RlclRlc3QgaXMgSG9va1Rlc3QgewogICAgdXNpbmcgUG9vbElkTGlicmFyeSBmb3IgUG9vbEtleTsKICAgIHVzaW5nIEN1cnJlbmN5TGlicmFyeSBmb3IgQ3VycmVuY3k7CgogICAgUG9vbEtleSBwb29sS2V5OwogICAgUG9vbElkIHBvb2xJZDsKICAgIFF1b3RlciBxdW90ZXI7CgogICAgZnVuY3Rpb24gc2V0VXAoKSBwdWJsaWMgewogICAgICAgIC8vIGNyZWF0ZXMgdGhlIHBvb2wgbWFuYWdlciwgdGVzdCB0b2tlbnMsIGFuZCBvdGhlciB1dGlsaXR5IHJvdXRlcnMKICAgICAgICBIb29rVGVzdC5pbml0SG9va1Rlc3RFbnYoKTsKICAgICAgICBxdW90ZXIgPSBuZXcgUXVvdGVyKGFkZHJlc3MobWFuYWdlcikpOwoKICAgICAgICAvLyBDcmVhdGUgdGhlIHBvb2wKICAgICAgICBwb29sS2V5ID0KICAgICAgICAgICAgUG9vbEtleShDdXJyZW5jeS53cmFwKGFkZHJlc3ModG9rZW4wKSksIEN1cnJlbmN5LndyYXAoYWRkcmVzcyh0b2tlbjEpKSwgMzAwMCwgNjAsIElIb29rcyhhZGRyZXNzKDB4MCkpKTsKICAgICAgICBwb29sSWQgPSBwb29sS2V5LnRvSWQoKTsKICAgICAgICBpbml0aWFsaXplUm91dGVyLmluaXRpYWxpemUocG9vbEtleSwgQ29uc3RhbnRzLlNRUlRfUkFUSU9fMV8xLCBaRVJPX0JZVEVTKTsKCiAgICAgICAgLy8gUHJvdmlkZSBsaXF1aWRpdHkgdG8gdGhlIHBvb2wKICAgICAgICBtb2RpZnlQb3NpdGlvblJvdXRlci5tb2RpZnlMaXF1aWRpdHkoCiAgICAgICAgICAgIHBvb2xLZXksCiAgICAgICAgICAgIElQb29sTWFuYWdlci5Nb2RpZnlMaXF1aWRpdHlQYXJhbXMoVGlja01hdGgubWluVXNhYmxlVGljayg2MCksIFRpY2tNYXRoLm1heFVzYWJsZVRpY2soNjApLCAxMDAwIGV0aGVyKSwKICAgICAgICAgICAgWkVST19CWVRFUwogICAgICAgICk7CiAgICB9CgogICAgZnVuY3Rpb24gdGVzdFF1b3Rlcl9vdXRwdXQoKSBwdWJsaWMgewogICAgICAgIHVpbnQxMjggYW1vdW50SW4gPSAxZTE4OwogICAgICAgIGJvb2wgemVyb0Zvck9uZSA9IHRydWU7CiAgICAgICAgdWludDE2MCBNQVhfU0xJUFBBR0UgPSB6ZXJvRm9yT25lID8gTUlOX1BSSUNFX0xJTUlUIDogTUFYX1BSSUNFX0xJTUlUOwoKICAgICAgICAvLyBnZXQgdGhlIHF1b3RlCiAgICAgICAgUG9vbEtleSBtZW1vcnkga2V5ID0gcG9vbEtleTsKICAgICAgICAoaW50MTI4W10gbWVtb3J5IGRlbHRhQW1vdW50cywgdWludDE2MCBzcXJ0UHJpY2VYOTZBZnRlciwpID0gcXVvdGVyLnF1b3RlRXhhY3RJbnB1dFNpbmdsZSgKICAgICAgICAgICAgSVF1b3Rlci5RdW90ZUV4YWN0U2luZ2xlUGFyYW1zKGtleSwgemVyb0Zvck9uZSwgYWRkcmVzcyh0aGlzKSwgYW1vdW50SW4sIE1BWF9TTElQUEFHRSwgWkVST19CWVRFUykKICAgICAgICApOwoKICAgICAgICAvLyBvdXRwdXQgaXMgYW1vdW50IDEKICAgICAgICBpbnQxMjggb3V0cHV0QW1vdW50ID0gZGVsdGFBbW91bnRzWzFdOwogICAgICAgIGNvbnNvbGUyLmxvZygiUXVvdGVkIG91dHB1dCBhbW91bnQ6ICIsIGludDI1NihvdXRwdXRBbW91bnQpKTsKCiAgICAgICAgLy8gUGVyZm9ybSBhIHRlc3Qgc3dhcAogICAgICAgIEJhbGFuY2VEZWx0YSBzd2FwRGVsdGEgPSBzd2FwKHBvb2xLZXksIGludDI1Nih1aW50MjU2KGFtb3VudEluKSksIHplcm9Gb3JPbmUsIFpFUk9fQllURVMpOwoKICAgICAgICAvLyBxdW90ZSBhZ3JlZXMgd2l0aCB0aGUgYWN0dWFsIHN3YXAKICAgICAgICBhc3NlcnRFcShvdXRwdXRBbW91bnQsIHN3YXBEZWx0YS5hbW91bnQxKCkpOwogICAgfQoKICAgIGZ1bmN0aW9uIHRlc3RRdW90ZXJfaW5wdXQoKSBwdWJsaWMgewogICAgICAgIHVpbnQxMjggYW1vdW50T3V0ID0gMWUxODsKICAgICAgICBib29sIHplcm9Gb3JPbmUgPSB0cnVlOwogICAgICAgIHVpbnQxNjAgTUFYX1NMSVBQQUdFID0gemVyb0Zvck9uZSA/IE1JTl9QUklDRV9MSU1JVCA6IE1BWF9QUklDRV9MSU1JVDsKCiAgICAgICAgLy8gZ2V0IHRoZSBxdW90ZQogICAgICAgIFBvb2xLZXkgbWVtb3J5IGtleSA9IHBvb2xLZXk7CiAgICAgICAgKGludDEyOFtdIG1lbW9yeSBkZWx0YUFtb3VudHMsIHVpbnQxNjAgc3FydFByaWNlWDk2QWZ0ZXIsKSA9IHF1b3Rlci5xdW90ZUV4YWN0T3V0cHV0U2luZ2xlKAogICAgICAgICAgICBJUXVvdGVyLlF1b3RlRXhhY3RTaW5nbGVQYXJhbXMoa2V5LCB6ZXJvRm9yT25lLCBhZGRyZXNzKHRoaXMpLCBhbW91bnRPdXQsIE1BWF9TTElQUEFHRSwgWkVST19CWVRFUykKICAgICAgICApOwoKICAgICAgICAvLyBpbnB1dCAocXVvdGVkKSBpcyBhbW91bnQgMAogICAgICAgIGludDEyOCBpbnB1dEFtb3VudCA9IGRlbHRhQW1vdW50c1swXTsKICAgICAgICBjb25zb2xlMi5sb2coIlF1b3RlZCBpbnB1dCBhbW91bnQ6ICIsIGludDI1NihpbnB1dEFtb3VudCkpOwoKICAgICAgICAvLyBQZXJmb3JtIGEgZXhhY3Qtb3V0cHV0IHN3YXAKICAgICAgICBCYWxhbmNlRGVsdGEgc3dhcERlbHRhID0gc3dhcChwb29sS2V5LCAtaW50MjU2KHVpbnQyNTYoYW1vdW50T3V0KSksIHplcm9Gb3JPbmUsIFpFUk9fQllURVMpOwogICAgICAgIGFzc2VydEVxKGlucHV0QW1vdW50LCBzd2FwRGVsdGEuYW1vdW50MCgpKTsKICAgICAgICAodWludDE2MCBzcXJ0UHJpY2VYOTYsLCkgPSBtYW5hZ2VyLmdldFNsb3QwKHBvb2xJZCk7CiAgICAgICAgYXNzZXJ0RXEoc3FydFByaWNlWDk2QWZ0ZXIsIHNxcnRQcmljZVg5Nik7CiAgICB9Cn0K", }, { - fileName: "TemplateSnippet.sol", - code: "aW1wb3J0IHtIb29rc30gZnJvbSAidjQtY29yZS9saWJyYXJpZXMvSG9va3Muc29sIjsKCmZ1bmN0aW9uIGdldEhvb2tzQ2FsbHMoKSBwdWJsaWMgcHVyZSBvdmVycmlkZSByZXR1cm5zIChIb29rcy5DYWxscyBtZW1vcnkpIHsKICAgIHJldHVybiBIb29rcy5DYWxscyh7CiAgICAgICAgYmVmb3JlSW5pdGlhbGl6ZTogZmFsc2UsCiAgICAgICAgYWZ0ZXJJbml0aWFsaXplOiBmYWxzZSwKICAgICAgICBiZWZvcmVNb2RpZnlQb3NpdGlvbjogZmFsc2UsCiAgICAgICAgYWZ0ZXJNb2RpZnlQb3NpdGlvbjogZmFsc2UsCiAgICAgICAgYmVmb3JlU3dhcDogdHJ1ZSwKICAgICAgICBhZnRlclN3YXA6IGZhbHNlLAogICAgICAgIGJlZm9yZURvbmF0ZTogZmFsc2UsCiAgICAgICAgYWZ0ZXJEb25hdGU6IGZhbHNlLAogICAgICAgIG5vT3A6IHRydWUgLy8gLS0gRU5BQkxFIE5PLU9QIC0tICAvLwogICAgfSk7Cn0=", + fileName: "QuoterSnippet.sol", + code: "aW1wb3J0IHtJUXVvdGVyfSBmcm9tICJ2NC1wZXJpcGhlcnkvaW50ZXJmYWNlcy9JUXVvdGVyLnNvbCI7CgpQb29sS2V5IG1lbW9yeSBrZXk7CnVpbnQxMjggYW1vdW50SW4gPSAxZTE4Owpib29sIHplcm9Gb3JPbmUgPSB0cnVlOwp1aW50MTYwIE1BWF9TTElQUEFHRSA9IHplcm9Gb3JPbmUgPyBNSU5fUFJJQ0VfTElNSVQgOiBNQVhfUFJJQ0VfTElNSVQ7CmJ5dGVzIG1lbW9yeSBob29rRGF0YTsKCi8vIGV4YWN0IGlucHV0IHdpbGwgcXVvdGUgZGVsdGFBbW91bnRzWzFdIChvdXRwdXQpCi8vIGV4YWN0IG91dHB1dCB3aWxsIHF1b3RlIGRlbHRhQW1vdW50c1swXSAoaW5wdXQpCihpbnQxMjhbXSBtZW1vcnkgZGVsdGFBbW91bnRzLCB1aW50MTYwIHNxcnRQcmljZVg5NkFmdGVyLCkgPSBxdW90ZXIucXVvdGVFeGFjdElucHV0U2luZ2xlKAogICAgSVF1b3Rlci5RdW90ZUV4YWN0U2luZ2xlUGFyYW1zKGtleSwgemVyb0Zvck9uZSwgYWRkcmVzcyh0aGlzKSwgYW1vdW50SW4sIE1BWF9TTElQUEFHRSwgaG9va0RhdGEpCik7Cg==", }, ] const html = `
Quoting swaps -- for offchain purposes
+Quoter performs a swap and reverts, this very expensive and should not be used onchain
+TEMPLATE INFO
+The Quoter
contract provides helper functions for quoting different types of swaps:
| | Exact Input | Exact Output |
+|-------------|-----------------------|------------------------|
+| Single Pool | quoteExactInputSingle | quoteExactOutputSingle |
+| Multi-hop | quoteExactInput | quoteExactOutput |
+
Exact Input
: Given the input
amount, how many output tokens can I expect
Exact Output
: Given the desired output
amount, how many input tokens should I provide
TEMPLATE
-// SPDX-License-Identifier: MIT
-pragma solidity ^0.8.20;
+Quoter snippet
+import {IQuoter} from "v4-periphery/interfaces/IQuoter.sol";
+
+PoolKey memory key;
+uint128 amountIn = 1e18;
+bool zeroForOne = true;
+uint160 MAX_SLIPPAGE = zeroForOne ? MIN_PRICE_LIMIT : MAX_PRICE_LIMIT;
+bytes memory hookData;
-import {BaseHook} from "@v4-by-example/utils/BaseHook.sol";
+// exact input will quote deltaAmounts[1] (output)
+// exact output will quote deltaAmounts[0] (input)
+(int128[] memory deltaAmounts, uint160 sqrtPriceX96After,) = quoter.quoteExactInputSingle(
+ IQuoter.QuoteExactSingleParams(key, zeroForOne, address(this), amountIn, MAX_SLIPPAGE, hookData)
+);
+
Example: Single Pool
+Please see testQuoter_output()
and testQuoter_input()
for example usage and validation
+// SPDX-License-Identifier: MIT
+pragma solidity ^0.8.19;
+import "forge-std/Test.sol";
+import {IHooks} from "v4-core/interfaces/IHooks.sol";
import {Hooks} from "v4-core/libraries/Hooks.sol";
+import {TickMath} from "v4-core/libraries/TickMath.sol";
import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol";
import {PoolKey} from "v4-core/types/PoolKey.sol";
+import {BalanceDelta} from "v4-core/types/BalanceDelta.sol";
import {PoolId, PoolIdLibrary} from "v4-core/types/PoolId.sol";
+import {Constants} from "v4-core/../test/utils/Constants.sol";
+import {CurrencyLibrary, Currency} from "v4-core/types/Currency.sol";
+import {HookTest} from "@v4-by-example/utils/HookTest.sol";
+import {IQuoter} from "v4-periphery/interfaces/IQuoter.sol";
+import {Quoter} from "v4-periphery/lens/Quoter.sol";
-contract NoOpSwap is BaseHook {
+contract QuoterTest is HookTest {
using PoolIdLibrary for PoolKey;
+ using CurrencyLibrary for Currency;
+
+ PoolKey poolKey;
+ PoolId poolId;
+ Quoter quoter;
+
+ function setUp() public {
+ // creates the pool manager, test tokens, and other utility routers
+ HookTest.initHookTestEnv();
+ quoter = new Quoter(address(manager));
+
+ // Create the pool
+ poolKey =
+ PoolKey(Currency.wrap(address(token0)), Currency.wrap(address(token1)), 3000, 60, IHooks(address(0x0)));
+ poolId = poolKey.toId();
+ initializeRouter.initialize(poolKey, Constants.SQRT_RATIO_1_1, ZERO_BYTES);
+
+ // Provide liquidity to the pool
+ modifyPositionRouter.modifyLiquidity(
+ poolKey,
+ IPoolManager.ModifyLiquidityParams(TickMath.minUsableTick(60), TickMath.maxUsableTick(60), 1000 ether),
+ ZERO_BYTES
+ );
+ }
- mapping(PoolId => uint256 count) public beforeSwapCount;
-
- constructor(IPoolManager _poolManager) BaseHook(_poolManager) {}
-
- function getHookPermissions() public pure override returns (Hooks.Permissions memory) {
- return Hooks.Permissions({
- beforeInitialize: false,
- afterInitialize: false,
- beforeModifyPosition: false,
- afterModifyPosition: false,
- beforeSwap: true, // -- No-op'ing the swap -- //
- afterSwap: false,
- beforeDonate: false,
- afterDonate: false,
- noOp: true, // -- ENABLE NO-OP -- //
- accessLock: false
- });
+ function testQuoter_output() public {
+ uint128 amountIn = 1e18;
+ bool zeroForOne = true;
+ uint160 MAX_SLIPPAGE = zeroForOne ? MIN_PRICE_LIMIT : MAX_PRICE_LIMIT;
+
+ // get the quote
+ PoolKey memory key = poolKey;
+ (int128[] memory deltaAmounts, uint160 sqrtPriceX96After,) = quoter.quoteExactInputSingle(
+ IQuoter.QuoteExactSingleParams(key, zeroForOne, address(this), amountIn, MAX_SLIPPAGE, ZERO_BYTES)
+ );
+
+ // output is amount 1
+ int128 outputAmount = deltaAmounts[1];
+ console2.log("Quoted output amount: ", int256(outputAmount));
+
+ // Perform a test swap
+ BalanceDelta swapDelta = swap(poolKey, int256(uint256(amountIn)), zeroForOne, ZERO_BYTES);
+
+ // quote agrees with the actual swap
+ assertEq(outputAmount, swapDelta.amount1());
}
- function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata params, bytes calldata)
- external
- override
- returns (bytes4)
- {
- // ------------------------------------------------------------------------------- //
- // Example NoOp: if swap amount is 69e18, then the swap will be skipped //
- // ------------------------------------------------------------------------------- //
- if (params.amountSpecified == 69e18) return Hooks.NO_OP_SELECTOR;
-
- beforeSwapCount[key.toId()]++;
- return BaseHook.beforeSwap.selector;
+ function testQuoter_input() public {
+ uint128 amountOut = 1e18;
+ bool zeroForOne = true;
+ uint160 MAX_SLIPPAGE = zeroForOne ? MIN_PRICE_LIMIT : MAX_PRICE_LIMIT;
+
+ // get the quote
+ PoolKey memory key = poolKey;
+ (int128[] memory deltaAmounts, uint160 sqrtPriceX96After,) = quoter.quoteExactOutputSingle(
+ IQuoter.QuoteExactSingleParams(key, zeroForOne, address(this), amountOut, MAX_SLIPPAGE, ZERO_BYTES)
+ );
+
+ // input (quoted) is amount 0
+ int128 inputAmount = deltaAmounts[0];
+ console2.log("Quoted input amount: ", int256(inputAmount));
+
+ // Perform a exact-output swap
+ BalanceDelta swapDelta = swap(poolKey, -int256(uint256(amountOut)), zeroForOne, ZERO_BYTES);
+ assertEq(inputAmount, swapDelta.amount0());
+ (uint160 sqrtPriceX96,,) = manager.getSlot0(poolId);
+ assertEq(sqrtPriceX96After, sqrtPriceX96);
}
}
-
Example TEMPLATE-SNIPPET
-import {Hooks} from "v4-core/libraries/Hooks.sol";
-
-function getHooksCalls() public pure override returns (Hooks.Calls memory) {
- return Hooks.Calls({
- beforeInitialize: false,
- afterInitialize: false,
- beforeModifyPosition: false,
- afterModifyPosition: false,
- beforeSwap: true,
- afterSwap: false,
- beforeDonate: false,
- afterDonate: false,
- noOp: true // -- ENABLE NO-OP -- //
- });
-}
`
export default html
diff --git a/src/pages/quoter/index.md b/src/pages/quoter/index.md
index 8e9c1f1e..386b8057 100644
--- a/src/pages/quoter/index.md
+++ b/src/pages/quoter/index.md
@@ -1,25 +1,38 @@
---
-title: TEMPLATE
+title: Quoter
version: 0.8.20
-description: TEMPLATE
-keywords: [template, example]
+description: Offchain Quoter, to fetch input/output amounts
+keywords: [quoter, quoting, exact input, exact output, single, multi, multihop]
---
-- SUBTITLE
+- Quoting swaps -- for **offchain purposes**
-TEMPLATE INFO
+- Quoter performs a swap and reverts, this very *expensive* and should not be used onchain
+
+The `Quoter` contract provides helper functions for quoting different types of swaps:
+
+```
+| | Exact Input | Exact Output |
+|-------------|-----------------------|------------------------|
+| Single Pool | quoteExactInputSingle | quoteExactOutputSingle |
+| Multi-hop | quoteExactInput | quoteExactOutput |
+```
+
+`Exact Input`: Given the `input` amount, how many *output* tokens can I expect
+
+`Exact Output`: Given the desired `output` amount, how many *input* tokens should I provide
---
-## Example TEMPLATE
+## Quoter snippet
-TEMPLATE
```solidity
-{{{Template}}}
+{{{QuoterSnippet}}}
```
-## Example TEMPLATE-SNIPPET
+## Example: Single Pool
+Please see `testQuoter_output()` and `testQuoter_input()` for example usage and validation
```solidity
-{{{TemplateSnippet}}}
+{{{Quoter}}}
```
diff --git a/src/routes.tsx b/src/routes.tsx
index e36e1c8f..11205f1e 100644
--- a/src/routes.tsx
+++ b/src/routes.tsx
@@ -3,6 +3,7 @@ import component_fees_fixed_hook_fee from "./pages/fees/fixed-hook-fee"
import component_hooks_custom_curve from "./pages/hooks/custom-curve"
import component_hooks_no_op from "./pages/hooks/no-op"
import component_initialize from "./pages/initialize"
+import component_quoter from "./pages/quoter"
import component_swap from "./pages/swap"
import component_template from "./pages/template"
import component_ from "./pages"
@@ -44,6 +45,10 @@ const routes: Route[] = [
path: "/initialize",
component: component_initialize
},
+ {
+ path: "/quoter",
+ component: component_quoter
+ },
{
path: "/swap",
component: component_swap
diff --git a/src/search.json b/src/search.json
index 58e334be..d9e7ba5d 100644
--- a/src/search.json
+++ b/src/search.json
@@ -15,6 +15,27 @@
"swapping": [
"/swap"
],
+ "quoter": [
+ "/quoter"
+ ],
+ "quoting": [
+ "/quoter"
+ ],
+ "exact input": [
+ "/quoter"
+ ],
+ "exact output": [
+ "/quoter"
+ ],
+ "single": [
+ "/quoter"
+ ],
+ "multi": [
+ "/quoter"
+ ],
+ "multihop": [
+ "/quoter"
+ ],
"pool": [
"/initialize"
],