Degen Code

Degen Code

Share this post

Degen Code
Degen Code
Uniswap V3 — Swap Callback
Copy link
Facebook
Email
Notes
More

Uniswap V3 — Swap Callback

We Use A Little Callback Humor In The Title

Nov 29, 2022
∙ Paid
7

Share this post

Degen Code
Degen Code
Uniswap V3 — Swap Callback
Copy link
Facebook
Email
Notes
More
Share

UniswapV2 offered a very simple method for doing swaps directly at the pool contract. The process was simple:

  • Send tokens

  • Call swap

  • Receive tokens

UniswapV3, to absolutely no one’s surprise, complicates the process by introducing callbacks for minting, swapping, and flash borrowing.

We are not strangers to the callback here at Degen Code. We first encountered them studying UniswapV2 flash borrowing, and then refined it by learning to pass arbitrary calldata through that same callback.

The bad news is that you absolutely cannot do a swap at the pool without including the callback in your smart contract.

There is a nice feature of the V3 callback that I have not seen discussed anywhere else:

With UniswapV2, calling swap at the pool will always execute at the amounts you specify. If you could have gotten a better swap, it will still give you the price you requested.

The V3 callback allows us to always get the best price for a given swap, since the required amount is calculated after the swap calculation is done. This is similar to the behavior of the router, where you submit a minimum output and often receive more.

With V3, you no longer have to pre-send the input amount. Rather, the swap input is paid afterwards.

Happily, the swap callback is fairly simple to implement. There is no guidance on how to properly protect the callback from nefarious use, but we can review the Router’s own callback implementation to see how they protect it.

function uniswapV3SwapCallback(
    int256 amount0Delta,
    int256 amount1Delta,
    bytes calldata _data
) external override {
    require(amount0Delta > 0 || amount1Delta > 0); // swaps entirely within 0-liquidity regions are not supported
    SwapCallbackData memory data = abi.decode(
        _data, 
        (SwapCallbackData)
    );
    (address tokenIn, address tokenOut, uint24 fee) = data.path.decodeFirstPool();
    CallbackValidation.verifyCallback(factory, tokenIn, tokenOut, fee);

    (bool isExactInput, uint256 amountToPay) =
        amount0Delta > 0
            ? (tokenIn < tokenOut, uint256(amount0Delta))
            : (tokenOut < tokenIn, uint256(amount1Delta));
    if (isExactInput) {
        pay(tokenIn, data.payer, msg.sender, amountToPay);
    } else {
        // either initiate the next swap or pay
        if (data.path.hasMultiplePools()) {
            data.path = data.path.skipToken();
            exactOutputInternal(amountToPay, msg.sender, 0, data);
        } else {
            amountInCached = amountToPay;
            tokenIn = tokenOut; // swap in/out because exact output swaps are reversed
            pay(tokenIn, data.payer, msg.sender, amountToPay);
        }
    }
}

If this is good enough for them, it’s good enough for us!

Callback Verification

This function does a few important checks, but we mainly care about the line:

CallbackValidation.verifyCallback(factory, tokenIn, tokenOut, fee);

This line defers the validation of the LP address (which should equal msg.sender) to the CallbackValidation library. The function verifyCallback executes this code:

pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey));
require(msg.sender == address(pool));

Where PoolAddress is another library.

The computeAddress function executes this somewhat complex check:

function computeAddress(address factory, PoolKey memory key) internal pure returns (address pool) {
    require(key.token0 < key.token1);
    pool = address(
        uint256(
            keccak256(
                abi.encodePacked(
                    hex'ff',
                    factory,
                    keccak256(abi.encode
                        (
                            key.token0, key.token1, key.fee
                        )
                    ),
                    POOL_INIT_CODE_HASH
                )
            )
        )
    );
}

This is pretty big and scary, but the purpose of the function is to generate a 20-byte pool address from a starting hash (POOL_INIT_CODE_HASH) and some inputs (factory, token0, token1, and fee).

It uses various Solidity built-ins including abi.encodePacked, keccak256, and type casting functions (uint256 and address).

We can implement this same function in Vyper:

@internal
@pure
def verifyCallback(
    tokenA: address, 
    tokenB: address, 
    fee: uint24
) -> address:   
            
    token0: address = tokenA
    token1: address = tokenB

    if convert(tokenA,uint160) > convert(tokenB,uint160):        
        token0 = tokenB
        token1 = tokenA
        
    return convert(
        slice(
            convert(
                convert(
                    keccak256(
                        concat(
                            b'\xFF',
                            convert(V3_FACTORY,bytes20),
                            keccak256(
                                _abi_encode(
                                    token0,
                                    token1,
                                    fee
                                )
                            ),
                            0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54,
                        )
                    ),
                    uint256
                ),
                bytes32
            ),
            12,
            20,
        ),
        address
    )

First, the function sorts the token addresses. token0 is always stored as the lower value. Vyper does not allow you to compare byte strings directly (Solidity does), so we compare the conversions to a uint256 value instead, then set the markers for each token.

Vyper does not have an abi.encodePacked function, but it’s simple to build that bytestring ourselves using concat.

Then we do a series of slices and conversions to transform our value into a 20 byte address, which is returned.

If this function is too exotic for you, you can also call getPool at the factory, but cross-contract calls are usually more expensive than doing the calculation on-contract. Up to you!

Vyper Implementation

With this built, let’s develop the callback function and implement a series of key checks. To verify that msg.sender is a legitimate pool, we ask it for three values via these functions:

  • token0

  • token1

  • fee

Then we ask the official UniswapV3 factory “what address do you have stored for token0, token1, and fee?”

If that answer matches msg.sender, we have a legitimate pool and can proceed with the rest of our swap callback.

Let’s build that now:

This post is for paid subscribers

Already a paid subscriber? Sign in
© 2025 BowTiedDevil
Privacy ∙ Terms ∙ Collection notice
Start writingGet the app
Substack is the home for great culture

Share

Copy link
Facebook
Email
Notes
More