ERC-6120: Universal Token Router

A singleton router contract allows tokens to be spent in the transfer-and-call pattern instead of approve-then-call.


Metadata
Status: ReviewStandards Track: ERCCreated: 2022-12-12
Authors
Derion (@derion-io), Zergity (@Zergity), Ngo Quang Anh (@anhnq82), BerlinP (@BerlinP), Khanh Pham (@blackskin18), Hal Blackburn (@h4l)

Abstract


The default transaction behavior of ETH is transfer-and-call, but the widely used ERC-20 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 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:


Output Verification

Output defines the expected token balance change for verification.


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, which can be used in output actions to verify the total amount of all ids owned by the recipient address.


Action

Action defines the token inputs and the contract call.


The action code contract MUST implement the NotToken contract or the ERC-165 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.


Input

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


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.


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


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


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.


Please refer to the Discard Payment 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.



Please refer to the Discard Payment 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 section in the Security Considerations for information on reentrancy risks and mitigation.

Usage Examples

Uniswap V2 Router

Legacy function:


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:


Uniswap V3 Router

Legacy router contract:


The helper contract to use with the UTR:


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


Allowance Adapter

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


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


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) 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.


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.


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.


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.


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.