---
eip: 6120
title: Universal Token Router
description: A singleton router contract allows tokens to be spent in the transfer-and-call pattern instead of approve-then-call.
author: Derion (@derion-io), Zergity (@Zergity), Ngo Quang Anh (@anhnq82), BerlinP (@BerlinP), Khanh Pham (@blackskin18), Hal Blackburn (@h4l)
discussions-to: https://ethereum-magicians.org/t/eip-6120-universal-token-router/12142
status: Review
type: Standards Track
category: ERC
created: 2022-12-12
requires: 20, 165, 721, 1014, 1155
---

## Abstract

The default transaction behavior of ETH is *transfer-and-call*, but the widely used [ERC-20](./eip-20.md) standard isn't compatible with this pattern. This incompatibility forces applications to use an inefficient and risky two-step *approve-then-call* process. This approach is costly, creates a poor user experience, and introduces significant security vulnerabilities, as users must approve unaudited and often upgradable contracts. This has led to numerous allowance-related bugs and exploits.

The **Universal Token Router** (**UTR**) addresses this issue by separating the token allowance from the application logic. This allows any token to be spent in a single contract call, similar to how ETH is handled, without needing to approve individual application contracts. When tokens are approved to the **UTR**, they can only be spent in transactions signed directly by the token owner. The **UTR**'s transaction data clearly shows key details like token types, amounts, and the recipient.

The **UTR** promotes the **security-by-result** model over the **security-by-process** model. By allowing applications to verify the output of a transaction (e.g., checking token balance changes), users' funds can be secure even when interacting with potentially flawed or malicious contracts.

The **UTR** contract is deployed at `0x69c4620b62D99f524c5B4dE45442FE2D7dD59576` on all EVM-compatible networks using the [EIP-1014](./eip-1014.md) SingletonFactory. This allows new token contracts to pre-configure it as a trusted spender, eliminating the need for approval transactions entirely for their interactive usage.

## Motivation

When users approve their tokens to a contract, they expect that:

* it only spends the tokens with their permission (from `msg.sender` or `ecrecover`)
* it does not use `delegatecall` (e.g. upgradable proxies)

The **UTR** ensures these same security conditions, allowing all interactive applications to share a single, secure token allowance. This saves most approval transactions for existing tokens and **all** approval transactions for new ones.

Before the **UTR**, users had to blindly trust the front-end code of applications to construct transactions honestly. This made them highly vulnerable to phishing. The **UTR**'s function arguments act as a manifest that wallets can display to users, allowing them to review the expected token behavior before signing, making phishing attacks much easier to detect.

Most existing application contracts are already compatible with the **UTR** and can integrate it to gain several benefits:

* Securely share a user's token allowance across all applications.
* Update their own peripheral contracts as often as needed without requiring new user approvals.
* Save development and security audit costs on their own router contracts.


## Specification

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119 and RFC 8174.

The main interface of the UTR contract:

```solidity
interface IUniversalTokenRouter {
    function exec(
        Output[] memory outputs,
        Action[] memory actions
    ) payable;
}
```

### Output Verification

`Output` defines the expected token balance change for verification.

```solidity
struct Output {
    address recipient;
    uint eip;           // token standard: 0 for ETH or EIP number
    address token;      // token contract address
    uint id;            // token id for ERC-721 and ERC-1155
    uint amountOutMin;
}
```

Token balances of the `recipient` address are recorded at the beginning and the end of the `exec` function for each item in `outputs`. Transaction will revert with `INSUFFICIENT_OUTPUT_AMOUNT` if any of the balance changes are less than its `amountOutMin`.

A special id `ERC_721_BALANCE` is reserved for [ERC-721](./eip-721.md), which can be used in output actions to verify the total amount of all ids owned by the `recipient` address.

```solidity
ERC_721_BALANCE = keccak256('UniversalTokenRouter.ERC_721_BALANCE')
```

### Action

`Action` defines the token inputs and the contract call.

```solidity
struct Action {
    Input[] inputs;
    address code;       // contract code address
    bytes data;         // contract input data
}
```

The action code contract MUST implement the `NotToken` contract or the [ERC-165](./eip-165.md) interface with the ID `0x61206120` in order to be called by the UTR. This interface check prevents the direct invocation of token *allowance-spending* functions (e.g., `transferFrom`) by the UTR. Therefore, new token contracts MUST NOT implement this interface ID.

```solidity
/**
 * This contract will conflict with the ERC20, ERC721, and ERC1155 standards,
 * preventing token contracts from accidentally implementing it.
 */
abstract contract NotToken  {
    function allowance(address, address) external pure returns (string memory) {
        return "THIS IS NOT A TOKEN";
    }
    function isApprovedForAll(address, address) external pure returns (string memory) {
        return "THIS IS NOT A TOKEN";
    }
}

contract Application is NotToken {
    // this contract can be used with the UTR
}
```

### Input

`Input` defines the input token to transfer or prepare before the action contract is executed.

```solidity
struct Input {
    uint mode;
    address recipient;
    uint eip;           // token standard: 0 for ETH or EIP number
    address token;      // token contract address
    uint id;            // token id for ERC-721 and ERC-1155
    uint amountIn;
}
```

`mode` takes one of the following values:

* `PAYMENT = 0`: pend a payment for the token to be transferred from `msg.sender` to the `recipient` by calling `UTR.pay` from anywhere in the same transaction.
* `TRANSFER = 1`: transfer the token directly from `msg.sender` to the `recipient`.
* `CALL_VALUE = 2`: record the `ETH` amount to pass to the action as the call `value`.

Each input in the `inputs` argument is processed sequentially. For simplicity, duplicated `PAYMENT` and `CALL_VALUE` inputs are valid, but only the last `amountIn` value is used.

#### Payment Input

`PAYMENT` is the recommended mode for application contracts that use the *transfer-in-callback* pattern. E.g., flashloan contracts, Uniswap/v3-core, Derion, etc.

For each `Input` with `PAYMENT` mode, at most `amountIn` of the token can be transferred from `msg.sender` to the `recipient` by calling `UTR.pay` from anywhere in the same transaction.

```
UTR
 |
 | PAYMENT
 | (payments pended for UTR.pay)
 |
 |                                  Application Contracts
action.code.call ---------------------> |
                                        |
UTR.pay <----------------------- (call) |
                                        |
 | <-------------------------- (return) |
 |
 | (clear all pending payments)
 |
END
```

Token's allowance and `PAYMENT` are essentially different as:

* allowance: allow a specific `spender` to transfer the token to anyone at any time.
* `PAYMENT`: allow anyone to transfer the token to a specific `recipient` only in that transaction.

##### Spend Payment

```solidity
interface IUniversalTokenRouter {
    function pay(bytes memory payment, uint amount);
}
```

To call `pay`, the `payment` param must be encoded as follows:

```solidity
payment = abi.encode(
    payer,      // address
    recipient,  // address
    eip,        // uint256
    token,      // address
    id          // uint256
);
```

The `payment` bytes can also be used by adapter UTR contracts to pass contexts and payloads for performing custom payment logic.

##### Discard Payment

Sometimes, it's useful to discard the payment instead of performing the transfer, for example, when the application contract wants to burn its own token from `payment.payer`. The following function can be used to verify the payment to the caller's address and discard a portion of it.

```solidity
interface IUniversalTokenRouter {
    function discard(bytes memory payment, uint amount);
}
```

Please refer to the [Discard Payment](#discard-payment-1) section in the **Security Considerations** for an important security note.

##### Sender Authentication

Discarding payment also makes sender authentication possible with a router, which is never achievable with regular routers. By inputting a pseudo payment (not a token payment), the UTR allows the target contract to verify the sender's address for authentication, along with normal token transfers and payments.

```solidity
contract AuthChecker is NotToken {
    // must be trusted with a proper implementation of discard function
    address immutable UTR;

    function actionMustSentBySender(address sender) external {
        bytes memory payment = abi.encode(sender, address(this), 0, address(0), 0);
        IUniversalTokenRouter(UTR).discard(payment, 1);
    }
}
```

```javascript
await utr.exec([], [{
    inputs: [{
        mode: PAYMENT,
        eip: 0,
        token: AddressZero,
        id: 0,
        amountIn: 1,
        recipient: paymentTest.address,
    }],
    code: authChecker.address,
    data: (await authChecker.populateTransaction.actionMustSentBySender(owner.address)).data,
}])
```

Please refer to the [Discard Payment](#discard-payment-1) section in the **Security Considerations** for an important security note.

##### Payment Lifetime

Payments are recorded in the UTR storage and intended to be spent by `input.action` external calls only within that transaction. All payment storages will be cleared before the `UTR.exec` ends.

### Native Token Tranfer

The `UTR` SHOULD have a `receive()` function for user execution logic that requires transferring ETH in. The `msg.value` transferred into the router can be spent in multiple inputs across different actions. While the caller takes full responsibility for the movement of `ETH` in and out of the router, the `exec` function SHOULD refund any remaining `ETH` before the function ends.

Please refer to the [Reentrancy](#reentrancy) section in the **Security Considerations** for information on reentrancy risks and mitigation.

### Usage Examples

#### Uniswap V2 Router

Legacy function:

```solidity
UniswapV2Router01.swapExactTokensForTokens(
    uint amountIn,
    uint amountOutMin,
    address[] calldata path,
    address to,
    uint deadline
)
```

`UniswapV2Helper01.swapExactTokensForTokens` is a modified version of it without the token transfer part.

This transaction is signed by users to execute the swap instead of the legacy function:

```javascript
UniversalTokenRouter.exec([{
    recipient: to,
    eip: 20,
    token: path[path.length-1],
    id: 0,
    amountOutMin,
}], [{
    inputs: [{
        mode: TRANSFER,
        recipient: UniswapV2Library.pairFor(factory, path[0], path[1]),
        eip: 20,
        token: path[0],
        id: 0,
        amountIn: amountIn,
    }],
    code: UniswapV2Helper01.address,
    data: encodeFunctionData("swapExactTokensForTokens", [
        amountIn,
        amountOutMin,
        path,
        to,
        deadline,
    ]),
}])
```

#### Uniswap V3 Router

Legacy router contract:

```solidity
contract SwapRouter {
    // this function is called by pool to pay the input tokens
    function pay(
        address token,
        address payer,
        address recipient,
        uint256 value
    ) internal {
        ...
        // pull payment
        TransferHelper.safeTransferFrom(token, payer, recipient, value);
    }
}
```

The helper contract to use with the `UTR`:

```solidity
contract SwapHelper {
    // this function is called by pool to pay the input tokens
    function pay(
        address token,
        address payer,
        address recipient,
        uint256 value
    ) internal {
        ...
        // pull payment
        bytes memory payment = abi.encode(payer, recipient, 20, token, 0);
        UTR.pay(payment, value);
    }
}
```

This transaction is signed by users to execute the `exactInput` functionality using `PAYMENT` mode:

```javascript
UniversalTokenRouter.exec([{
    eip: 20,
    token: tokenOut,
    id: 0,
    amountOutMin: 1,
    recipient: to,
}], [{
    inputs: [{
        mode: PAYMENT,
        eip: 20,
        token: tokenIn,
        id: 0,
        amountIn: amountIn,
        recipient: pool.address,
    }],
    code: SwapHelper.address,
    data: encodeFunctionData("exactInput", [...]),
}])
```

#### Allowance Adapter

A simple non-reentrancy ERC-20 adapter for aplication and router contracts that use direct allowance.

```solidity
contract AllowanceAdapter is ReentrancyGuard {
    struct Input {
        address token;
        uint amountIn;
    }

    function approveAndCall(
        Input[] memory inputs,
        address spender,
        bytes memory data,
        address leftOverRecipient
    ) external payable nonReentrant {
        for (uint i = 0; i < inputs.length; ++i) {
            Input memory input = inputs[i];
            IERC20(input.token).approve(spender, input.amountIn);
        }

        (bool success, bytes memory result) = spender.call{value: msg.value}(data);
        if (!success) {
            assembly {
                revert(add(result, 32), mload(result))
            }
        }

        for (uint i = 0; i < inputs.length; ++i) {
            Input memory input = inputs[i];
            // clear all allowance
            IERC20(input.token).approve(spender, 0);
            uint leftOver = IERC20(input.token).balanceOf(address(this));
            if (leftOver > 0) {
                TransferHelper.safeTransfer(input.token, leftOverRecipient, leftOver);
            }
        }
    }
}
```

This transaction is constructed to utilize the `UTR` to interact with Uniswap V2 Router without approving any token to it:

```javascript
const { data: routerData } = await uniswapRouter.populateTransaction.swapExactTokensForTokens(
    amountIn,
    amountOutMin,
    path,
    to,
    deadline,
)

const { data: adapterData } = await adapter.populateTransaction.approveAndCall(
    [{
        token: path[0],
        amountIn,
    }],
    uniswapRouter.address,
    routerData,
    leftOverRecipient,
)

await utr.exec([], [{
    inputs: [{
        mode: TRANSFER,
        recipient: adapter.address,
        eip: 20,
        token: path[0],
        id: 0,
        amountIn,
    }],
    code: adapter.address,
    data: adapterData,
}])
```

## Rationale

The `Permit` type signature is not supported since the purpose of the Universal Token Router is to eliminate all interactive `approve` signatures for new tokens, and *most* for old tokens.

## Backwards Compatibility

### Tokens

Old token contracts (ERC-20, ERC-721 and [ERC-1155](./eip-1155.md)) require approval for the Universal Token Router once for each account.

New token contracts can pre-configure the Universal Token Router as a trusted spender, and no approval transaction is required for interactive usage.

```solidity
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

/**
 * @dev Implementation of the {ERC20} token standard that support a trusted ERC6120 contract as an unlimited spender.
 */
contract ERC20WithUTR is ERC20 {
    address immutable UTR;

    /**
     * @dev Sets the values for {name}, {symbol} and ERC6120's {utr} address.
     *
     * All three of these values are immutable: they can only be set once during
     * construction.
     *
     * @param utr can be zero to disable trusted ERC6120 support.
     */
    constructor(string memory name, string memory symbol, address utr) ERC20(name, symbol) {
        UTR = utr;
    }

    /**
     * @dev See {IERC20-allowance}.
     */
    function allowance(address owner, address spender) public view virtual override returns (uint256) {
        if (spender == UTR && spender != address(0)) {
            return type(uint256).max;
        }
        return super.allowance(owner, spender);
    }

    /**
     * Does not check or update the allowance if `spender` is the UTR.
     */
    function _spendAllowance(address owner, address spender, uint256 amount) internal virtual override {
        if (spender == UTR && spender != address(0)) {
            return;
        }
        super._spendAllowance(owner, spender, amount);
    }
}
```

### Applications

The only application contracts **INCOMPATIBLE** with the UTR are contracts that use `msg.sender` as the beneficiary address in their internal storage without any function for ownership transfer.

All application contracts that accept `recipient` (or `to`) argument as the beneficiary address are compatible with the UTR out of the box.

Application contracts that transfer tokens (ERC-20, ERC-721, and ERC-1155) to `msg.sender` need additional adapters to add a `recipient` to their functions.

```solidity
// sample adapter contract for WETH
contract WethAdapter {
    function deposit(address recipient) external payable {
        IWETH(WETH).deposit(){value: msg.value};
        TransferHelper.safeTransfer(WETH, recipient, msg.value);
    }
}
```

Additional helper and adapter contracts might be needed, but they're mostly peripheral and non-intrusive. They don't hold any tokens or allowances, so they can be frequently updated and have little to no security impact on the core application contracts.

## Reference Implementation

A reference implementation by Derion Labs and audited by Hacken.

```solidity
/// @title The implementation of the EIP-6120.
/// @author Derion Labs
contract UniversalTokenRouter is ERC165, IUniversalTokenRouter {
    uint256 constant PAYMENT       = 0;
    uint256 constant TRANSFER      = 1;
    uint256 constant CALL_VALUE    = 2;

    uint256 constant EIP_ETH       = 0;

    uint256 constant ERC_721_BALANCE = uint256(keccak256('UniversalTokenRouter.ERC_721_BALANCE'));

    /// The main entry point of the router
    /// @param outputs token behavior for output verification
    /// @param actions router actions and inputs for execution
    function exec(
        Output[] memory outputs,
        Action[] memory actions
    ) external payable virtual override {
    unchecked {
        // track the expected balances before any action is executed
        for (uint256 i = 0; i < outputs.length; ++i) {
            Output memory output = outputs[i];
            uint256 balance = _balanceOf(output);
            uint256 expected = output.amountOutMin + balance;
            require(expected >= balance, 'UTR: OUTPUT_BALANCE_OVERFLOW');
            output.amountOutMin = expected;
        }

        for (uint256 i = 0; i < actions.length; ++i) {
            Action memory action = actions[i];
            uint256 value;
            for (uint256 j = 0; j < action.inputs.length; ++j) {
                Input memory input = action.inputs[j];
                uint256 mode = input.mode;
                if (mode == CALL_VALUE) {
                    // eip and id are ignored
                    value = input.amountIn;
                } else {
                    if (mode == PAYMENT) {
                        bytes32 key = keccak256(abi.encode(
                            msg.sender, input.recipient, input.eip, input.token, input.id
                        ));
                        uint amountIn = input.amountIn;
                        assembly {
                            tstore(key, amountIn)
                        }
                    } else if (mode == TRANSFER) {
                        _transferToken(msg.sender, input.recipient, input.eip, input.token, input.id, input.amountIn);
                    } else {
                        revert('UTR: INVALID_MODE');
                    }
                }
            }
            if (action.code != address(0) || action.data.length > 0 || value > 0) {
                require(
                    TokenChecker.isNotToken(action.code) ||
                    ERC165Checker.supportsInterface(action.code, 0x61206120),
                    "UTR: NOT_CALLABLE"
                );
                (bool success, bytes memory result) = action.code.call{value: value}(action.data);
                if (!success) {
                    assembly {
                        revert(add(result,32),mload(result))
                    }
                }
            }
            // clear all transient storages
            for (uint256 j = 0; j < action.inputs.length; ++j) {
                Input memory input = action.inputs[j];
                if (input.mode == PAYMENT) {
                    // transient storages
                    bytes32 key = keccak256(abi.encode(
                        msg.sender, input.recipient, input.eip, input.token, input.id
                    ));
                    assembly {
                        tstore(key, 0)
                    }
                }
            }
        }

        // refund any left-over ETH
        uint256 leftOver = address(this).balance;
        if (leftOver > 0) {
            TransferHelper.safeTransferETH(msg.sender, leftOver);
        }

        // verify balance changes
        for (uint256 i = 0; i < outputs.length; ++i) {
            Output memory output = outputs[i];
            uint256 balance = _balanceOf(output);
            // NOTE: output.amountOutMin is reused as `expected`
            require(balance >= output.amountOutMin, 'UTR: INSUFFICIENT_OUTPUT_AMOUNT');
        }
    } }
    
    /// Spend the pending payment. Intended to be called from the input.action.
    /// @param payment encoded payment data
    /// @param amount token amount to pay with payment
    function pay(bytes memory payment, uint256 amount) external virtual override {
        discard(payment, amount);
        (
            address sender,
            address recipient,
            uint256 eip,
            address token,
            uint256 id
        ) = abi.decode(payment, (address, address, uint256, address, uint256));
        _transferToken(sender, recipient, eip, token, id, amount);
    }

    /// Discard a part of a pending payment. Can be called from the input.action
    /// to verify the payment without transferring any token.
    /// @param payment encoded payment data
    /// @param amount token amount to pay with payment
    function discard(bytes memory payment, uint256 amount) public virtual override {
        bytes32 key = keccak256(payment);
        uint256 remain;
        assembly {
            remain := tload(key)
        }
        require(remain >= amount, 'UTR: INSUFFICIENT_PAYMENT');
        assembly {
            tstore(key, sub(remain, amount))
        }
    }

    // IERC165-supportsInterface
    function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
        return
            interfaceId == type(IUniversalTokenRouter).interfaceId ||
            super.supportsInterface(interfaceId);
    }

    function _transferToken(
        address sender,
        address recipient,
        uint256 eip,
        address token,
        uint256 id,
        uint256 amount
    ) internal virtual {
        if (eip == 20) {
            TransferHelper.safeTransferFrom(token, sender, recipient, amount);
        } else if (eip == 1155) {
            IERC1155(token).safeTransferFrom(sender, recipient, id, amount, "");
        } else if (eip == 721) {
            IERC721(token).safeTransferFrom(sender, recipient, id);
        } else {
            revert("UTR: INVALID_EIP");
        }
    }

    function _balanceOf(
        Output memory output
    ) internal view virtual returns (uint256 balance) {
        uint256 eip = output.eip;
        if (eip == 20) {
            return IERC20(output.token).balanceOf(output.recipient);
        }
        if (eip == 1155) {
            return IERC1155(output.token).balanceOf(output.recipient, output.id);
        }
        if (eip == 721) {
            if (output.id == ERC_721_BALANCE) {
                return IERC721(output.token).balanceOf(output.recipient);
            }
            try IERC721(output.token).ownerOf(output.id) returns (address currentOwner) {
                return currentOwner == output.recipient ? 1 : 0;
            } catch {
                return 0;
            }
        }
        if (eip == EIP_ETH) {
            return output.recipient.balance;
        }
        revert("UTR: INVALID_EIP");
    }
}
```

## Security Considerations

### ERC-165 Tokens

Token contracts must **NEVER** support the ERC-165 interface with the ID `0x61206120`, as it is reserved for non-token contracts to be called with the UTR. Any token with the interface ID `0x61206120` approved to the UTR can be spent by anyone, without any restrictions.

### Reentrancy

Tokens transferred to the UTR contract will be permanently lost, as there is no way to transfer them out. Applications that require an intermediate address to hold tokens should use their own Helper contract with a reentrancy guard for secure execution.

ETH must be transferred to the UTR contracts before the value is spent in an action call (using `CALL_VALUE`). This ETH value can be siphoned out of the UTR using a re-entrant call inside an action code or rogue token functions. This exploit will not be possible if users don't transfer more ETH than they will spend in that transaction.

```solidity
// transfer 100 in, but spend only 60,
// so at most 40 wei can be exploited in this transaction
UniversalTokenRouter.exec([
    ...
], [{
    inputs: [{
        mode: CALL_VALUE,
        eip: 20,
        token: 0,
        id: 0,
        amountIn: 60,   // spend 60
        recipient: AddressZero,
    }],
    ...
}], {
    value: 100,   // transfer 100 in
})
```

### Discard Payment

The result of the `pay` function can be checked by querying the balance after the call, allowing the UTR contract to be called in a trustless manner. However, due to the inability to verify the execution of the `discard` function, it should only be used with a trusted UTR contract.

## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).
