ERC-6120 Universal token router

Review · Other · created 2022-12-12

A shared, single router contract that lets any token be used in one call, just like ETH. Instead of approving each app separately, you only need to approve this one router.

Why it is needed

ETH can be sent and a contract executed at the same time, but widely used ERC-20 tokens cannot do this. So to use an app, you must first send an approval transaction authorizing token use, then send a separate execution transaction, a two-step process. This approach is costly and provides a poor user experience, and because token spending permission must be entrusted to contracts that may be unaudited or upgradeable, it has caused many allowance-related bugs and theft incidents. Users also have no way to verify whether an app's frontend constructed a transaction honestly, leaving them exposed to phishing. This standard identifies the coupling of token approval and app logic as the problem and proposes a shared router to separate the two.

What to implement

In depth

A single exec call completes input, execution, and verification

exec(outputs, actions) is the sole entry point. The Output struct defines the expected balance change via recipient, eip (0 for ETH, otherwise the EIP number), token, id, and amountOutMin. UTR records the recipient's balance at the start and end of exec, and reverts with INSUFFICIENT_OUTPUT_AMOUNT if the change is less than amountOutMin. For ERC-721, a reserved special id, ERC_721_BALANCE = keccak256('UniversalTokenRouter.ERC_721_BALANCE'), lets you verify the recipient's total holdings instead of a specific id. Because the result is verified rather than the process (security-by-result), even if a faulty contract is used along the way, the transaction becomes invalid if the expected tokens are not received.

Action calls and the mechanism preventing token miscalls

An Action consists of inputs (input tokens), code (the contract address to call), and data (the call data). The action's code contract MUST implement the NotToken contract or the ERC-165 interface ID 0x61206120 for UTR to call it. This check is a safeguard to prevent UTR from directly calling allowance-spending functions such as transferFrom. NotToken deliberately conflicts with the ERC-20, ERC-721, and ERC-1155 standards by overriding allowance and isApprovedForAll to return strings. Therefore, new token contracts MUST NOT implement this interface ID.

Three input modes

The Input struct consists of mode, recipient, eip, token, id, and amountIn, and there are three modes. PAYMENT(0) reserves a payment within the same transaction, allowing anyone to call UTR.pay to move up to amountIn from msg.sender to recipient. TRANSFER(1) sends tokens immediately from msg.sender to recipient. CALL_VALUE(2) records the amount of ETH (value) to pass to the action call. Inputs are processed in order, and if PAYMENT and CALL_VALUE are duplicated, only the last amountIn is valid. Allowance and PAYMENT are different. An allowance lets a given spender send to anyone at any time, while PAYMENT only allows sending to the recipient fixed within that transaction.

pay, discard, and the lifetime of a payment

The payment argument of pay(payment, amount) is encoded as abi.encode(payer, recipient, eip, token, id). PAYMENT mode is recommended for transfer-in-callback patterns, such as flash loans or Uniswap v3, where tokens are pulled inside a callback. discard(payment, amount) is a function that destroys a reservation without moving tokens, used when an app burns the payer's own tokens, or for sender authentication by destroying a single fake, non-token payment unit to verify that a caller-specified address is the actual signer. All payment records are stored in UTR storage and are entirely cleared before exec finishes, so they cannot be used outside the transaction.

Singleton deployment and approval-free new tokens

UTR is deployed at the same address, 0x69c4620b62D99f524c5B4dE45442FE2D7dD59576, on every EVM-compatible network via the EIP-1014 SingletonFactory. Existing tokens (ERC-20, ERC-721, ERC-1155) only need to approve UTR once per account, and new tokens, as in the ERC20WithUTR example, can eliminate the approval transaction entirely by having allowance return type(uint256).max and skipping _spendAllowance when the spender is UTR. Permit-style signatures are not supported, because the purpose of this standard is to eliminate all approval signatures for new tokens and most approvals for existing tokens.

Compatibility with existing apps

App contracts that take a beneficiary address as a recipient (or to) argument are compatible as-is, with no modification needed. The only incompatible contracts are those that record msg.sender internally as the beneficiary with no way to transfer ownership. Contracts that send tokens to msg.sender need an adapter that adds a recipient, as in the WethAdapter example, and existing routers that use allowances directly can be connected via AllowanceAdapter's approveAndCall. Since these helpers and adapters do not hold tokens or allowances, updating them frequently has little impact on the core app contract's security.

A proposal still under review

The original's status is Review, meaning it is a proposal stage whose content may still change. The original includes a reference implementation written by Derion Labs and audited by Hacken.

Things to watch out for

Original ERC · ← ERCs