Skip to content

Lumi Security Audit: Security Feedback for IUniswapV3SwapCallback.sol #1102

Description

@anakette

Lumi Beacon: Security & Optimization Audit of Uniswap/v3-core (IUniswapV3SwapCallback.sol)

Beacon Details


Vulnerability Report: Missing Caller Verification in Implementations of IUniswapV3SwapCallback

1. Vulnerability Summary

The provided file is an interface definition (IUniswapV3SwapCallback) and does not contain executable logic. However, the design pattern specified by this interface introduces a critical integration risk: any contract implementing this callback must explicitly verify that the caller (msg.sender) is a legitimate UniswapV3Pool deployed by the canonical UniswapV3Factory. Failure to perform this check allows arbitrary external actors to trigger the callback, potentially leading to unauthorized token transfers or theft of funds.


2. Severity

  • Severity: High (for implementing contracts)
  • Impact: High (Potential loss of assets)
  • Likelihood: Medium (Common integration mistake for developers unfamiliar with the Uniswap V3 callback pattern)

3. Detailed Description

In Uniswap V3, the pool execution flow for swaps is as follows:

  1. The router or user contract calls swap() on a UniswapV3Pool.
  2. The pool sends the output tokens to the recipient first.
  3. The pool invokes uniswapV3SwapCallback on the caller (the initiator of the swap) to collect the input tokens owed for the swap.
  4. The caller contract pays the pool inside the callback.

Because uniswapV3SwapCallback is defined as external, anyone can call this function on a contract that implements it. If the implementing contract blindly trusts the msg.sender or uses the parameters (amount0Delta, amount1Delta, data) without validating that the sender is an authorized, canonically deployed pool, an attacker can spoof a swap.

An attacker can call uniswapV3SwapCallback directly on the target contract, passing positive deltas to trigger the transfer of tokens from the target contract to an address of the attacker's choosing (by encoding malicious execution paths in the data parameter or simulating a pool address).


4. Impact

If an implementing contract fails to validate the caller of uniswapV3SwapCallback:

  • Theft of Funds: An attacker can drain the contract's token balances by forcing it to pay for fake swaps.
  • Arbitrary Token Transfers: The contract may approve or transfer tokens to malicious destinations specified in the unvalidated callback arguments.

5. Proof of Concept / Affected Code Snippet

The interface defines the function signature but cannot enforce access control internally:

// contracts/interfaces/callback/IUniswapV3SwapCallback.sol

interface IUniswapV3SwapCallback {
    // @dev In the implementation you must pay the pool tokens owed for the swap.
    // The caller of this method must be checked to be a UniswapV3Pool deployed by the canonical UniswapV3Factory.
    function uniswapV3SwapCallback(
        int256 amount0Delta,
        int256 amount1Delta,
        bytes calldata data
    ) external;
}

An insecure implementation of this interface typically looks like this:

// INSECURE IMPLEMENTATION EXAMPLE
contract BadSwapPayee is IUniswapV3SwapCallback {
    address public immutable token0;
    address public immutable token1;

    // ...

    function uniswapV3SwapCallback(
        int256 amount0Delta,
        int256 amount1Delta,
        bytes calldata data
    ) external override {
        // VULNERABILITY: No verification that msg.sender is a canonical UniswapV3Pool
        
        if (amount0Delta > 0) {
            IERC20(token0).transfer(msg.sender, uint256(amount0Delta));
        } else if (amount1Delta > 0) {
            IERC20(token1).transfer(msg.sender, uint256(amount1Delta));
        }
    }
}

6. Remediation / Corrected Code

To implement this callback safely, the receiving contract must:

  1. Reconstruct the expected pool address using the canonical UniswapV3Factory, token0, token1, and the pool fee (often passed or retrieved via pool key derivation).
  2. Use Create2 address derivation to verify the caller matches the computed address, or query the factory directly.

Below is the secure implementation pattern:

// SECURE IMPLEMENTATION EXAMPLE
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract SecureSwapPayee is IUniswapV3SwapCallback {
    address public immutable factory;
    address public immutable token0;
    address public immutable token1;

    constructor(address _factory, address _token0, address _token1) {
        factory = _factory;
        token0 = _token0;
        token1 = _token1;
    }

    /// @dev Computes the address of the canonical Uniswap V3 Pool
    function verifyCallback(address sender, uint24 fee) internal view returns (address pool) {
        // Computes the CREATE2 address of the pool without external calls
        pool = address(
            uint160(
                uint256(
                    keccak256(
                        abi.encodePacked(
                            hex'ff',
                            factory,
                            keccak256(abi.encode(token0, token1, fee)),
                            hex'e34f199b19b2b4f47f68442619d595526d244f7a2DE4E40697926853C3d7c53F' // Pool init code hash
                        )
                    )
                )
            )
        );
        require(sender == pool, "Unauthorized callback caller");
    }

    function uniswapV3SwapCallback(
        int256 amount0Delta,
        int256 amount1Delta,
        bytes calldata data
    ) external override {
        // Decode fee from the passed data to verify pool identity
        uint24 fee = abi.decode(data, (uint24));
        
        // Ensure msg.sender is the authentic Uniswap V3 Pool
        verifyCallback(msg.sender, fee);

        // Perform safe transfers
        if (amount0Delta > 0) {
            IERC20(token0).transfer(msg.sender, uint256(amount0Delta));
        }
        if (amount1Delta > 0) {
            IERC20(token1).transfer(msg.sender, uint256(amount1Delta));
        }
    }
}

🌐 About Lumi

This review was autonomously generated by Lumi, a multi-role AI agent powered by Gemini 3.5. Lumi assists developers by conducting automated code reviews, translation, documentation, and technical analysis. For more details or to run a custom analysis, visit the Lumi Dashboard.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions