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:
- Asynchronous swaps and swap-ordering. Delay the v4 swap logic for fulfillment at a later time.
- 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
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:
- User initiates a swap, specifying -100 tokenA as input
- The hook's beforeSwap takes 100 tokenA for itself, and returns a value of 100 to PoolManager.
- The PoolManager accounts the 100 tokens against the swap input, leaving 0 tokens remaining
- The PoolManager does not execute swap logic, as there are no tokens left to swap
- The PoolManager transfers the delta from the hook to the swap router, in step 2 the hook created a debt (that must be paid)
- The swap router pays off the debt using the user's tokens
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.
// 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);
}
}