Skip to content

Conversation

@leovigna
Copy link

Proposal: Add CALL_TARGET Command

TLDR

Add a new CALL_TARGET Command that enables users to make a custom call at any time during the execution.

Overview

The UniversalRouter provides significant efficiency for interacting across Uniswap versions & Permit2 approvals. Users can in a single transaction do any number of approvals/transfers/swaps across any number of Uniswap Pools (v2/v3/v4). This enables even simple EOAs the power to batch financial transactions.

Problem: Custom Logic with Uniswap

If an EOA wants to interact with a different contract not supported by the UniversalRouter however, they cannot do so and must rely on some other external periphery contract.

  • Combining Uniswap transactions with other contracts often requires custom periphery contracts
  • Developers often build a less-gas efficient solution that adds additional token transfers
  • Users often have to approve an additional contract that pulls the funds, executes swaps, and then custom logic

Solution: Interoperability via Universal Router

The CALL_TARGET command enables calling a custom contract at any time during the UniversalRouter's execution. Adding a CALL_TARGET command enables great interoperability between Uniswap & custom contracts by simply encoding a custom call at any point between the command execution.

This unlocks several use cases:

  • Call a bridge contract post-swap to send tokens to some other chain
  • Call a registry contract to track user's trading volume using delta accounting (track pre/post swap balance)
  • Call a contract that expects payment in user's output currency

Our team at ETHDenver significantly used this feature in our Veraswap MVP to easily combine Uniswap with post-swap bridging via Hyperlane or Superchain Interop.

Purpose of PR

Purposes for this PR is to get some feedback of the CALL_TARGET command:

  1. Any thoughts on the concept of the CALL_TARGET command?
  2. Any suggestions regarding security? Would the Uniswap Foundation Security Fund or any other resources be open to sponsoring an audit?
  3. Is there a possibility of getting this merged if the future (post-audit) or would this be better suited as a standalone router implementation? What would be the best path forward?

Implementation

CALL_TARGET Command

We update the Commands.sol with a custom CALL_TARGET with the 0x22 flag.

Call Target Decoder

We add the CalldataCallTargetDecoder.sol library to decode the call target params. This library was added to avoid editing the CallDataDecoder.sol in v4-periphery.

We decode the following params abi.decode(params, (address, uint256, bytes)):

  • address: target address
  • uint256: call value
  • bytes: call data

Dispatcher Logic

The Dispatcher.sol contract implements the core of the Universal Router and is responsible for decoding and executing the logic of each command. At the bottom of the execute command, we add the implementation for CALL_TARGET right before the // placeholder area for commands 0x22-0x3f comment.

The Dispatcher implementation does the following:

  1. Decode command
  2. Check if target is BANNED Permit2 address
  3. Execute call
    // ...Previous commands
    else if (command == Commands.CALL_TARGET) {
        // CALL_TARGET: Call target contract with data
        (address target, uint256 value, bytes calldata data) = CalldataCallTargetDecoder.decodeCallTarget(
            inputs
        );
        // Call target cannot be PERMIT2 to avoid arbitrary token transfers
        if (target == address(PERMIT2)) revert CallTargetPermit2();

        (success, output) = payable(target).call{value: value}(data);
    } else {
        // placeholder area for commands 0x22-0x3f
        revert InvalidCommandType(command);
    }

Security Considerations

The main reason we can add a custom CALL_TARGET with little security changes is because one main assumption is that the UniversalRouter does not hold funds between transactions (anyone can SWEEP) and that approvals are managed via Permit2.

For example, an attacker can have the UniversalRouter call ERC20.approve(<attacker>, MAX_UINT256) to try to steal any balances from the router, but the same can already be achieved by a simple SWEEP command. The main risk is managing user approvals, and since they are exclusively managed via Permit2, banning the Permit2 address makes any attack on 3rd party approvals impossible.

Ban Permit2
The only security check is that the target is NOT Permit2. This is critical since token transfers to and from UniversalRouter are managed via Permit2. If Permit2 was allowed, a attacker could call the Permit2 contract and steal a 3rd party's approved funds.
Interactions with Permit2 contract are tightly controlled by the PERMIT2 commands and always use the proper caller.

Banning Other Addresses
Other addresses could also be banned for good measure but do not seem necessary:

  • v2/v3/v4 Protocol Addresses: Callers can encode any type of swap already and the router is not designed to hold funds between transactions anyways
  • Self: Self-rentrancy is already allowed via the EXECUTE_SUBPLAN command

@leovigna
Copy link
Author

PS: Added this PR as a draft to get the conversation going on any thoughts regarding adding this feature to the official Uniswap UniversalRouter

@OscBacon
Copy link

OscBacon commented Apr 1, 2025

+1

@leovigna leovigna force-pushed the feat/add-call-target branch from 3ede106 to fb6900a Compare September 5, 2025 16:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants