AsyncSwap Hooks
One feature enabled by custom accounting is AsyncSwap 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 custom accounting:
- 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.
AsyncSwap 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 AsyncSwap means hook developers can implement harmful behavior (such as taking all swap amounts for themselves, charging extra fees, etc.). Hooks with AsyncSwap behavior should be examined very closely by both developers and users.
Configure a AsyncSwap Hook
To enable AsyncSwap, developers will need the hook permission BEFORE_SWAP_RETURNS_DELTA_FLAG
import {BaseHook} from "v4-periphery/BaseHook.sol";
// ...
contract AsyncSwapHook 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
AsyncSwap 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 AsyncSwapHook is BaseHook {
// ...
function beforeSwap(address, PoolKey calldata key, IPoolManager.SwapParams calldata params, bytes calldata)
external
override
returns (bytes4, BeforeSwapDelta, uint24)
{
// AsyncSwap 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 AsyncSwap 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 AsyncSwap 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 AsyncSwapTest is Test, Deployers {
// ...
function setUp() public {
// ...
}
function test_asyncSwap() 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 (AsyncSwap)
assertEq(balance1Before, balance1After);
}
}