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 = ` -

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


-

Example TEMPLATE

-

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" ],