-
Notifications
You must be signed in to change notification settings - Fork 546
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add NoOp hooks guide with mdx formatting (#756)
- Loading branch information
1 parent
754d3e9
commit c27116d
Showing
1 changed file
with
131 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,131 @@ | ||
--- | ||
title: NoOp Hooks | ||
--- | ||
|
||
One feature enabled by custom accounting is NoOp swap. This feature allows hook developers to replace the v4 (v3-style) swap logic. | ||
|
||
This means developers can replace Uniswap's internal core logic for how to handle swaps. Two emergent use-cases are possible with NoOp: | ||
|
||
1. Asynchronous swaps and swap-ordering. Delay the v4 swap logic for fulfillment at a later time. | ||
2. Custom Curves. Replace the v4 swap logic with different swap logic. The custom logic is flexible and developers can implement symmetric curves, asymmetric curves, or custom quoting. | ||
|
||
> NoOp is typically described as taking the full input to replace the internal swap logic, partially taking the input is better described as *custom accounting* | ||
Note: The flexibility of NoOp means hook developers can implement harmful behavior (such as taking all swap amounts for themselves, charging extra fees, etc.). Hooks with NoOp behavior should be examined very closely by both developers and users. | ||
|
||
# Configure a NoOp Hook | ||
|
||
To enable NoOp, developers will need the hook permission `BEFORE_SWAP_RETURNS_DELTA_FLAG` | ||
|
||
```solidity | ||
import {BaseHook} from "v4-periphery/BaseHook.sol"; | ||
// ... | ||
contract NoOpHook is BaseHook { | ||
// ... | ||
function getHookPermissions() public pure virtual override returns (Hooks.Permissions memory) { | ||
return Hooks.Permissions({ | ||
beforeInitialize: false, | ||
afterInitialize: false, | ||
beforeAddLiquidity: false, | ||
afterAddLiquidity: false, | ||
beforeRemoveLiquidity: false, | ||
afterRemoveLiquidity: false, | ||
beforeSwap: false, | ||
afterSwap: true, | ||
beforeDonate: false, | ||
afterDonate: false, | ||
beforeSwapReturnDelta: true, | ||
afterSwapReturnDelta: false, | ||
afterAddLiquidityReturnDelta: false, | ||
afterRemoveLiquidityReturnDelta: false | ||
}); | ||
} | ||
// ... | ||
} | ||
``` | ||
|
||
# beforeSwap | ||
|
||
NoOp only works on exact-input swaps and the *beforeSwap* **must** take the input currency and return `BeforeSwapDelta`. The hook should `IPoolManager.mint` itself the corresponding tokens equal to the amount of the input (`amountSpecified`). It should then return a `BeforeSwapDelta` where `deltaSpecified = -amountSpecified` (the positive amount). | ||
|
||
The funds' movements are as follows: | ||
|
||
1. User initiates a swap, specifying -100 tokenA as input | ||
2. The hook's beforeSwap takes 100 tokenA for itself, and returns a value of 100 to PoolManager. | ||
3. The PoolManager accounts the 100 tokens against the swap input, leaving 0 tokens remaining | ||
4. The PoolManager does not execute swap logic, as there are no tokens left to swap | ||
5. The PoolManager transfers the delta from the hook to the swap router, in step 2 the hook created a debt (that must be paid) | ||
6. The swap router pays off the debt using the user's tokens | ||
|
||
```solidity | ||
contract NoOpHook is BaseHook { | ||
// ... | ||
function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata params, bytes calldata) | ||
external | ||
override | ||
returns (bytes4, BeforeSwapDelta, uint24) | ||
{ | ||
// NoOp only works on exact-input swaps | ||
if (params.amountSpecified < 0) { | ||
// take the input token so that v3-swap is skipped... | ||
Currency input = params.zeroForOne ? key.currency0 : key.currency1; | ||
uint256 amountTaken = uint256(-params.amountSpecified); | ||
poolManager.mint(address(this), input.toId(), amountTaken); | ||
// to NoOp the exact input, we return the amount that's taken by the hook | ||
return (BaseHook.beforeSwap.selector, toBeforeSwapDelta(amountTaken.toInt128(), 0), 0); | ||
} | ||
else { | ||
return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO, 0); | ||
} | ||
} | ||
} | ||
``` | ||
|
||
# Testing | ||
|
||
To verify the NoOp behaved properly, developers should test the swap and that token balances match expected behavior. | ||
|
||
```solidity | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.19; | ||
import "forge-std/Test.sol"; | ||
import {Deployers} from "v4-core/test/utils/Deployers.sol"; | ||
// ... | ||
contract NoOpSwapTest is Test, Deployers { | ||
// ... | ||
function setUp() public { | ||
// ... | ||
} | ||
function test_noOp() public { | ||
assertEq(hook.beforeSwapCount(poolId), 0); | ||
uint256 balance0Before = currency0.balanceOfSelf(); | ||
uint256 balance1Before = currency1.balanceOfSelf(); | ||
// Perform a test swap // | ||
int256 amount = -1e18; | ||
bool zeroForOne = true; | ||
BalanceDelta swapDelta = swap(poolKey, zeroForOne, amount, ZERO_BYTES); | ||
// ------------------- // | ||
uint256 balance0After = currency0.balanceOfSelf(); | ||
uint256 balance1After = currency1.balanceOfSelf(); | ||
// user paid token0 | ||
assertEq(balance0Before - balance0After, 1e18); | ||
// user did not recieve token1 (NoOp) | ||
assertEq(balance1Before, balance1After); | ||
} | ||
} | ||
``` |