845 lines
30 KiB
Markdown
845 lines
30 KiB
Markdown
|
---
|
||
|
eip: 6120
|
||
|
title: Universal Token Router
|
||
|
description: A single router contract enables tokens to be sent to application contracts in the transfer-and-call manner instead of approve-then-call.
|
||
|
author: Zergity (@Zergity), Ngo Quang Anh (@anhnq82), BerlinP (@BerlinP), Khanh Pham (@blackskin18)
|
||
|
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, 721, 1014, 1155
|
||
|
---
|
||
|
|
||
|
## Abstract
|
||
|
|
||
|
ETH is designed with transfer-and-call as the default behavior in a transaction. Unfortunately, [ERC-20](./eip-20.md) is not designed with that pattern in mind and newer standards cannot apply to the token contracts that have already been deployed.
|
||
|
|
||
|
Application and router contracts have to use the approve-then-call pattern which costs additional `n*m*l` `allow` (or `permit`) transactions, for `n` contracts, `m` tokens, and `l` user addresses. These allowance transactions not only cost a lot of user gas, worsen user experience, waste network storage and throughput, but they also put users at serious security risks as they often have to approve unaudited, unverified and upgradable proxy contracts.
|
||
|
|
||
|
The Universal Token Router (UTR) separates the token allowance from the application logic, allowing any token to be spent in a contract call the same way with ETH, without approving any other application contracts.
|
||
|
|
||
|
Tokens approved to the Universal Token Router can only be spent in transactions directly signed by their owner, and they have clearly visible token transfer behavior, including token types (ETH, [ERC-20](./eip-20.md), [ERC-721](./eip-721.md) or [ERC-1155](./eip-1155.md)), `amountInMax`, `amountOutMin`, and `recipient`.
|
||
|
|
||
|
The Universal Token Router contract is counter-factually deployed using [EIP-1014](./eip-1014.md) at `0x6120245B546F2F0ce439186cAae8181007366120` across all EVM-compatible networks, so new token contracts can pre-configure it as a trusted spender and no approval transaction is necessary.
|
||
|
|
||
|
## Motivation
|
||
|
|
||
|
When users approve their tokens to a contract, they trust that:
|
||
|
|
||
|
* it only spends the tokens with their permission (from `msg.sender` or `ecrecover`)
|
||
|
* it does not use `delegatecall` (e.g. upgradable proxies)
|
||
|
|
||
|
By performing the same security conditions above, the Universal Token Router can be shared by all applications, saving `(n-1)*m*l` approval transactions for old tokens and **ALL** approval transactions for new tokens.
|
||
|
|
||
|
Before this EIP, when users sign transactions to spend their approved tokens, they trust the front-end code entirely to construct those transactions honestly and correctly. This puts them at great risk of phishing sites.
|
||
|
|
||
|
The Universal Token Router function arguments can act as a manifest for users when signing a transaction. With the support from wallets, users can see and review their expected token behavior instead of blindly trusting the application contracts and front-end code. Phishing sites will be much easier to detect and avoid for users.
|
||
|
|
||
|
Most of the application contracts are already compatible with the Universal Token Router and can use it to have the following benefits:
|
||
|
|
||
|
* Safely share the user token allowance with all other applications.
|
||
|
* Freely update their helper contract logic.
|
||
|
* Save development and security audit costs on router contracts.
|
||
|
|
||
|
The Universal Token Router promotes the **security-by-result** model in decentralized applications instead of **security-by-process**. By directly querying token balance change for output verification, user transactions can be secured even when interacting with erroneous or malicious contracts. With non-token results, application helper contracts can provide additional result-checking functions for UTR's output verification.
|
||
|
|
||
|
## 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
|
||
|
) external 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 `ID_721_ALL` 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.
|
||
|
|
||
|
```solidity
|
||
|
ID_721_ALL = keccak256('UniversalTokenRouter.ID_721_ALL')
|
||
|
```
|
||
|
|
||
|
### Action
|
||
|
|
||
|
`Action` defines the token inputs and the contract call.
|
||
|
|
||
|
```solidity
|
||
|
struct Action {
|
||
|
Input[] inputs;
|
||
|
uint flags;
|
||
|
address code; // contract code address
|
||
|
bytes data; // contract input data
|
||
|
}
|
||
|
```
|
||
|
|
||
|
`flags` can take any number of the following bit flags:
|
||
|
|
||
|
* `0x1 = ACTION_IGNORE_ERROR`: any contract call failure will be ignored.
|
||
|
* `0x2 = ACTION_RECORD_CALL_RESULT`: the contract call result will be recorded in a `bytes` for subsequent actions.
|
||
|
* `0x4 = ACTION_INJECT_CALL_RESULT`: the last call result `bytes` recorded will be injected to the last empty `bytes` param of the contract function `data`.
|
||
|
|
||
|
### 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 amountInMax;
|
||
|
uint amountSource; // where to get the actual amountIn
|
||
|
}
|
||
|
```
|
||
|
|
||
|
`mode` can takes one of the following values:
|
||
|
|
||
|
* `0 = TRANSFER_FROM_SENDER`: the token will be transferred from `msg.sender` to `recipient`.
|
||
|
* `1 = TRANSFER_FROM_ROUTER`: the token will be transferred from `this` UTR contract to `recipient`.
|
||
|
* `2 = TRANSFER_CALL_VALUE`: the token amount will be passed to the action as the call `value`.
|
||
|
* `4 = IN_TX_PAYMENT`: the token will be allowed to be spent in this transaction by calling `UTR.pay`.
|
||
|
* `8 = ALLOWANCE_BRIDGE`: the token will be transferred from `msg.sender` to `this` UTR contract and is allowed to be spent in this transaction.
|
||
|
|
||
|
`amountSource` defines how the actual token `amountIn` is acquired from:
|
||
|
|
||
|
* `0 = AMOUNT_EXACT`: the `amountInMax` value is used.
|
||
|
* `1 = AMOUNT_ALL`: the entire balance of the sender (`msg.sender` or `this`) is used.
|
||
|
* otherwise, extracts the `uint256` value starting from the `amountSource`-th byte of the last recorded call result `bytes`. This value is unpredictable if there's no prior action with the `ACTION_RECORD_CALL_RESULT` flag.
|
||
|
|
||
|
`amountIn` MUST NOT be greater than `amountInMax`, otherwise, the transaction will be reverted with `EXCESSIVE_INPUT_AMOUNT`.
|
||
|
|
||
|
#### Payment In Callback
|
||
|
|
||
|
`IN_TX_PAYMENT` is used for application contracts that use the transfer-in-callback pattern. (E.g. flashloan contracts, Uniswap/v3-core, etc.)
|
||
|
|
||
|
```solidity
|
||
|
interface IUniversalTokenRouter {
|
||
|
...
|
||
|
|
||
|
function pay(
|
||
|
address sender,
|
||
|
address recipient,
|
||
|
uint eip,
|
||
|
address token,
|
||
|
uint id,
|
||
|
uint amount
|
||
|
) external;
|
||
|
}
|
||
|
```
|
||
|
|
||
|
For each `Input` with `IN_TX_PAYMENT` mode, at most `amountIn` of the token is allowed to be transferred from `msg.sender` to the `recipient` by calling `UTR.pay` from anywhere in the same transaction.
|
||
|
|
||
|
```
|
||
|
UTR
|
||
|
|
|
||
|
| IN_TX_PAYMENT
|
||
|
| (payments pended for UTR.pay)
|
||
|
|
|
||
|
| Application Contracts
|
||
|
action.code.call ---------------------> |
|
||
|
|
|
||
|
UTR.pay <----------------------- (call) |
|
||
|
|
|
||
|
| <-------------------------- (return) |
|
||
|
|
|
||
|
| (clear all pending payments)
|
||
|
|
|
||
|
END
|
||
|
```
|
||
|
|
||
|
#### Allowance Bridge
|
||
|
|
||
|
`ALLOWANCE_BRIDGE` is the compatibility mode for application contracts that require token approval directly from `msg.sender`.
|
||
|
|
||
|
For each `Input` with `ALLOWANCE_BRIDGE` mode:
|
||
|
|
||
|
* an `amountIn` of token is transferred from `msg.sender` to `this` UTR contract.
|
||
|
* the `recipient` address is allowed to spend the token from `this` UTR contract.
|
||
|
|
||
|
Before the end of the `exec` function:
|
||
|
|
||
|
* all allowances are revoked.
|
||
|
* all left-over tokens are transferred back to `msg.sender`.
|
||
|
|
||
|
### Usage Samples
|
||
|
|
||
|
#### `UniswapRouter.swapExactTokensForTokens`
|
||
|
|
||
|
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_FROM_SENDER,
|
||
|
recipient: UniswapV2Library.pairFor(factory, path[0], path[1]),
|
||
|
eip: 20,
|
||
|
token: path[0],
|
||
|
id: 0,
|
||
|
amountInMax: amountIn,
|
||
|
amountSource: AMOUNT_EXACT,
|
||
|
}],
|
||
|
flags: 0,
|
||
|
code: UniswapV2Helper01.address,
|
||
|
data: encodeFunctionData("swapExactTokensForTokens", [
|
||
|
amountIn,
|
||
|
amountOutMin,
|
||
|
path,
|
||
|
to,
|
||
|
deadline,
|
||
|
]),
|
||
|
}])
|
||
|
```
|
||
|
|
||
|
#### `UniswapRouter.swapTokensForExactTokens`
|
||
|
|
||
|
Legacy function:
|
||
|
|
||
|
```solidity
|
||
|
UniswapV2Router01.swapTokensForExactTokens(
|
||
|
uint amountOut,
|
||
|
uint amountInMax,
|
||
|
address[] calldata path,
|
||
|
address to,
|
||
|
uint deadline
|
||
|
)
|
||
|
```
|
||
|
|
||
|
This function accepts the `uint[] amounts` as the last `bytes` param, decode and pass to the internal function `_swap` of `UniswapV2Helper01`.
|
||
|
|
||
|
```solidity
|
||
|
UniswapV2Helper01.swap(
|
||
|
address[] calldata path,
|
||
|
address to,
|
||
|
bytes calldata amountsBytes
|
||
|
) external {
|
||
|
uint[] memory amounts = abi.decode(amountsBytes, (uint[]));
|
||
|
_swap(amounts, path, to);
|
||
|
}
|
||
|
```
|
||
|
|
||
|
This transaction is signed by users to execute the swap instead of the legacy function:
|
||
|
|
||
|
```javascript
|
||
|
UniversalTokenRouter.exec([{
|
||
|
eip: 20,
|
||
|
token: path[path.length-1],
|
||
|
id: 0,
|
||
|
amountOutMin: amountOut,
|
||
|
recipient: to,
|
||
|
}], [{
|
||
|
inputs: [],
|
||
|
flags: ACTION_RECORD_CALL_RESULT,
|
||
|
code: UniswapV2Helper01.address,
|
||
|
data: encodeFunctionData("getAmountIns", [amountOut, path]),
|
||
|
}, {
|
||
|
inputs: [{
|
||
|
mode: TRANSFER_FROM_SENDER,
|
||
|
eip: 20,
|
||
|
token: path[0],
|
||
|
id: 0,
|
||
|
amountInMax,
|
||
|
amountSource: 32*3, // first item of getAmountIns result array
|
||
|
recipient: UniswapV2Library.pairFor(factory, path[0], path[1]),
|
||
|
}],
|
||
|
flags: ACTION_INJECT_CALL_RESULT,
|
||
|
code: UniswapV2Helper01.address,
|
||
|
data: encodeFunctionData("swap", [path, to, '0x']),
|
||
|
}])
|
||
|
```
|
||
|
|
||
|
The result of `getAmountIns` is recorded and injected into the empty `bytes`, save the transaction from calculating twice with the same data.
|
||
|
|
||
|
#### `UniswapRouter.addLiquidity`
|
||
|
|
||
|
Legacy function:
|
||
|
|
||
|
```solidity
|
||
|
UniswapV2Router01.addLiquidity(
|
||
|
address tokenA,
|
||
|
address tokenB,
|
||
|
uint amountADesired,
|
||
|
uint amountBDesired,
|
||
|
uint amountAMin,
|
||
|
uint amountBMin,
|
||
|
address to,
|
||
|
uint deadline
|
||
|
)
|
||
|
```
|
||
|
|
||
|
This transaction is signed by users instead of the legacy function:
|
||
|
|
||
|
```javascript
|
||
|
UniversalTokenRouter.exec([{
|
||
|
eip: 20,
|
||
|
token: UniswapV2Library.pairFor(factory, tokenA, tokenB),
|
||
|
id: 0,
|
||
|
amountOutMin: 1, // just enough to verify the correct recipient
|
||
|
recipient: to,
|
||
|
}], [{
|
||
|
inputs: [],
|
||
|
flags: ACTION_RECORD_CALL_RESULT,
|
||
|
code: UniswapV2Helper01.address,
|
||
|
data: encodeFunctionData("_addLiquidity", [
|
||
|
tokenA,
|
||
|
tokenB,
|
||
|
amountADesired,
|
||
|
amountBDesired,
|
||
|
amountAMin,
|
||
|
amountBMin,
|
||
|
]),
|
||
|
}, {
|
||
|
inputs: [{
|
||
|
mode: TRANSFER_FROM_SENDER,
|
||
|
eip: 20,
|
||
|
token: tokenA,
|
||
|
id: 0,
|
||
|
amountSource: 32, // first item of _addLiquidity results
|
||
|
amountInMax: amountADesired,
|
||
|
recipient: UniswapV2Library.pairFor(factory, tokenA, tokenB),
|
||
|
}, {
|
||
|
mode: TRANSFER_FROM_SENDER,
|
||
|
eip: 20,
|
||
|
token: tokenB,
|
||
|
id: 0,
|
||
|
amountSource: 64, // second item of _addLiquidity results
|
||
|
amountInMax: amountBDesired,
|
||
|
recipient: UniswapV2Library.pairFor(factory, tokenA, tokenB),
|
||
|
}],
|
||
|
flags: 0,
|
||
|
code: UniswapV2Library.pairFor(factory, tokenA, tokenB),
|
||
|
data: encodeFunctionData("mint", [to]),
|
||
|
}])
|
||
|
```
|
||
|
|
||
|
The output token verification is not performed by Uniswap's legacy function and can be skipped. But it SHOULD always be done for the `UniversalTokenRouter` so user can see and review the token behavior instead of blindly trust the front-end code.
|
||
|
|
||
|
#### Uniswap V3 `SwapRouter`
|
||
|
|
||
|
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
|
||
|
UTR.pay(
|
||
|
payer,
|
||
|
recipient,
|
||
|
20, // EIP
|
||
|
token,
|
||
|
0, // id
|
||
|
value
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
```
|
||
|
|
||
|
This transaction is signed by users to execute the `exactInput` functionality using `IN_TX_PAYMENT` mode:
|
||
|
|
||
|
```javascript
|
||
|
UniversalTokenRouter.exec([{
|
||
|
eip: 20,
|
||
|
token: tokenOut,
|
||
|
id: 0,
|
||
|
amountOutMin: 1,
|
||
|
recipient: to,
|
||
|
}], [{
|
||
|
inputs: [{
|
||
|
mode: IN_TX_PAYMENT,
|
||
|
eip: 20,
|
||
|
token: tokenIn,
|
||
|
id: 0,
|
||
|
amountSource: AMOUNT_EXACT,
|
||
|
amountInMax: amountIn,
|
||
|
recipient: pool.address,
|
||
|
}],
|
||
|
flags: 0,
|
||
|
code: SwapHelper.address,
|
||
|
data: encodeFunctionData("exactInput", [...]),
|
||
|
}])
|
||
|
```
|
||
|
|
||
|
This transaction is signed by users to execute the `mint` functionality using `ALLOWANCE_BRIDGE` mode:
|
||
|
|
||
|
```javascript
|
||
|
UniversalTokenRouter.exec([{
|
||
|
eip: 721,
|
||
|
token: PositionManager.address,
|
||
|
id: ID_721_ALL,
|
||
|
amountOutMin: 1, // expect one more liquidity NFT
|
||
|
recipient: to,
|
||
|
}], [{
|
||
|
inputs: [{
|
||
|
mode: ALLOWANCE_BRIDGE,
|
||
|
eip: 20,
|
||
|
token: tokenA,
|
||
|
id: 0,
|
||
|
amountSource: AMOUNT_EXACT,
|
||
|
amountInMax: amountADesired,
|
||
|
recipient: PositionManager.address,
|
||
|
}, {
|
||
|
mode: ALLOWANCE_BRIDGE,
|
||
|
eip: 20,
|
||
|
token: tokenB,
|
||
|
id: 0,
|
||
|
amountSource: AMOUNT_EXACT,
|
||
|
amountInMax: amountBDesired,
|
||
|
recipient: PositionManager.address,
|
||
|
}],
|
||
|
flags: 0,
|
||
|
code: PositionManager.address,
|
||
|
data: encodeFunctionData("mint", [...]),
|
||
|
}])
|
||
|
```
|
||
|
|
||
|
## Rationale
|
||
|
|
||
|
The `Permit` type signature is not supported since the purpose of the Universal Token Router is to eliminate all `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.
|
||
|
|
||
|
### Application Contracts
|
||
|
|
||
|
Application contracts that use `msg.sender` as the beneficiary address in their internal storage without any function for ownership transfer are the only cases that are **INCOMPATIBLE** with the UTR.
|
||
|
|
||
|
All application contracts that accept `recipient` (or `to`) argument instead of using `msg.sender` 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` can use the `TRANSFER_FROM_ROUTER` input mode to re-direct tokens to another `recipient` address.
|
||
|
|
||
|
```javascript
|
||
|
// sample code to deposit WETH and transfer them out
|
||
|
UniversalTokenRouter.exec([{
|
||
|
eip: 20,
|
||
|
token: WETH.address,
|
||
|
id: 0,
|
||
|
amountOutMin: 1,
|
||
|
recipient: SomeRecipient,
|
||
|
}], [{
|
||
|
inputs: [{
|
||
|
mode: TRANSFER_CALL_VALUE,
|
||
|
eip: 0, // ETH
|
||
|
token: AddressZero,
|
||
|
id: 0,
|
||
|
amountInMax: 123,
|
||
|
amountSource: AMOUNT_EXACT,
|
||
|
recipient: AddressZero, // pass it as the value for the next output action
|
||
|
}],
|
||
|
flags: 0,
|
||
|
code: WETH.address,
|
||
|
data: encodeFunctionData('deposit', []), // WETH.deposit returns WETH token to the UTR contract
|
||
|
}, {
|
||
|
inputs: [{
|
||
|
mode: TRANSFER_FROM_ROUTER, // transfer token out from this UTR contract
|
||
|
eip: 20,
|
||
|
token: WETH.address,
|
||
|
id: 0,
|
||
|
amountInMax: 123,
|
||
|
amountSource: AMOUNT_ALL, // entire WETH balance of this UTR contract
|
||
|
recipient: SomeRecipient,
|
||
|
}],
|
||
|
// ... continue to use WETH in SomeRecipient
|
||
|
flags: 0,
|
||
|
code: AddressZero,
|
||
|
data: '0x',
|
||
|
}], {value: 123})
|
||
|
```
|
||
|
|
||
|
Applications can also deploy additional adapter contracts to add a `recipient` to their functions.
|
||
|
|
||
|
```solidity
|
||
|
// sample adapter contract for WETH
|
||
|
contract WethAdapter {
|
||
|
address immutable WETH = 0x....;
|
||
|
function deposit(address recipient) external payable {
|
||
|
IWETH(WETH).deposit(){value: msg.value};
|
||
|
TransferHelper.safeTransfer(WETH, recipient, msg.value);
|
||
|
}
|
||
|
}
|
||
|
```
|
||
|
|
||
|
## Reference Implementation
|
||
|
|
||
|
```solidity
|
||
|
contract UniversalTokenRouter is IUniversalTokenRouter {
|
||
|
// values with a single 1-bit are preferred
|
||
|
uint constant TRANSFER_FROM_SENDER = 0;
|
||
|
uint constant TRANSFER_FROM_ROUTER = 1;
|
||
|
uint constant TRANSFER_CALL_VALUE = 2;
|
||
|
uint constant IN_TX_PAYMENT = 4;
|
||
|
uint constant ALLOWANCE_BRIDGE = 8;
|
||
|
|
||
|
uint constant AMOUNT_EXACT = 0;
|
||
|
uint constant AMOUNT_ALL = 1;
|
||
|
|
||
|
uint constant EIP_ETH = 0;
|
||
|
|
||
|
uint constant ID_721_ALL = uint(keccak256('UniversalTokenRouter.ID_721_ALL'));
|
||
|
|
||
|
uint constant ACTION_IGNORE_ERROR = 1;
|
||
|
uint constant ACTION_RECORD_CALL_RESULT = 2;
|
||
|
uint constant ACTION_INJECT_CALL_RESULT = 4;
|
||
|
|
||
|
// non-persistent in-transaction pending payments
|
||
|
mapping(bytes32 => uint) s_payments;
|
||
|
|
||
|
// accepting ETH for WETH.withdraw
|
||
|
receive() external payable {}
|
||
|
|
||
|
function exec(
|
||
|
Output[] memory outputs,
|
||
|
Action[] memory actions
|
||
|
) override external payable {
|
||
|
unchecked {
|
||
|
// track the expected balances before any action is executed
|
||
|
for (uint i = 0; i < outputs.length; ++i) {
|
||
|
Output memory output = outputs[i];
|
||
|
uint balance = _balanceOf(output.recipient, output.eip, output.token, output.id);
|
||
|
uint expected = output.amountOutMin + balance;
|
||
|
require(expected >= balance, 'UniversalTokenRouter: OVERFLOW');
|
||
|
output.amountOutMin = expected;
|
||
|
}
|
||
|
|
||
|
bool dirty = false;
|
||
|
|
||
|
bytes memory callResult;
|
||
|
for (uint i = 0; i < actions.length; ++i) {
|
||
|
Action memory action = actions[i];
|
||
|
uint value;
|
||
|
for (uint j = 0; j < action.inputs.length; ++j) {
|
||
|
Input memory input = action.inputs[j];
|
||
|
uint mode = input.mode;
|
||
|
address sender = mode == TRANSFER_FROM_ROUTER ? address(this) : msg.sender;
|
||
|
uint amount;
|
||
|
if (input.amountSource == AMOUNT_EXACT) {
|
||
|
amount = input.amountInMax;
|
||
|
} else {
|
||
|
if (input.amountSource == AMOUNT_ALL) {
|
||
|
amount = _balanceOf(sender, input.eip, input.token, input.id);
|
||
|
} else {
|
||
|
amount = _sliceUint(callResult, input.amountSource);
|
||
|
}
|
||
|
require(amount <= input.amountInMax, "UniversalTokenRouter: EXCESSIVE_INPUT_AMOUNT");
|
||
|
}
|
||
|
if (mode == TRANSFER_CALL_VALUE) {
|
||
|
value = amount;
|
||
|
continue;
|
||
|
}
|
||
|
if (mode == TRANSFER_FROM_SENDER || mode == TRANSFER_FROM_ROUTER) {
|
||
|
_transferToken(sender, input.recipient, input.eip, input.token, input.id, amount);
|
||
|
continue;
|
||
|
}
|
||
|
if (mode == IN_TX_PAYMENT) {
|
||
|
bytes32 key = keccak256(abi.encodePacked(msg.sender, input.recipient, input.eip, input.token, input.id));
|
||
|
s_payments[key] += amount; // overflow: harmless
|
||
|
dirty = true;
|
||
|
continue;
|
||
|
}
|
||
|
if (mode == ALLOWANCE_BRIDGE) {
|
||
|
_approve(input.recipient, input.eip, input.token, type(uint).max);
|
||
|
_transferToken(msg.sender, address(this), input.eip, input.token, input.id, amount);
|
||
|
dirty = true;
|
||
|
}
|
||
|
}
|
||
|
if (action.data.length > 0) {
|
||
|
if (action.flags & ACTION_INJECT_CALL_RESULT != 0) {
|
||
|
action.data = _concat(action.data, action.data.length, callResult);
|
||
|
}
|
||
|
(bool success, bytes memory result) = action.code.call{value: value}(action.data);
|
||
|
if (!success && action.flags & ACTION_IGNORE_ERROR == 0) {
|
||
|
assembly {
|
||
|
revert(add(result,32),mload(result))
|
||
|
}
|
||
|
}
|
||
|
// delete value; // clear the ETH value after call
|
||
|
if (action.flags & ACTION_RECORD_CALL_RESULT != 0) {
|
||
|
callResult = result;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// verify balance changes
|
||
|
for (uint i = 0; i < outputs.length; ++i) {
|
||
|
Output memory output = outputs[i];
|
||
|
uint balance = _balanceOf(output.recipient, output.eip, output.token, output.id);
|
||
|
require(balance >= output.amountOutMin, 'UniversalTokenRouter: INSUFFICIENT_OUTPUT_AMOUNT');
|
||
|
}
|
||
|
|
||
|
// clear all in-transaction storages
|
||
|
if (dirty) {
|
||
|
for (uint i = 0; i < actions.length; ++i) {
|
||
|
Action memory action = actions[i];
|
||
|
for (uint j = 0; j < action.inputs.length; ++j) {
|
||
|
Input memory input = action.inputs[j];
|
||
|
if (input.mode == IN_TX_PAYMENT) {
|
||
|
bytes32 key = keccak256(abi.encodePacked(msg.sender, input.recipient, input.eip, input.token, input.id));
|
||
|
delete s_payments[key];
|
||
|
continue;
|
||
|
}
|
||
|
if (input.mode == ALLOWANCE_BRIDGE) {
|
||
|
_approve(input.recipient, input.eip, input.token, 0);
|
||
|
uint balance = _balanceOf(address(this), input.eip, input.token, input.id);
|
||
|
if (balance > 0) {
|
||
|
_transferToken(address(this), msg.sender, input.eip, input.token, input.id, balance);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// refund any left-over ETH
|
||
|
uint leftOver = address(this).balance;
|
||
|
if (leftOver > 0) {
|
||
|
TransferHelper.safeTransferETH(msg.sender, leftOver);
|
||
|
}
|
||
|
} }
|
||
|
|
||
|
function pay(
|
||
|
address sender,
|
||
|
address recipient,
|
||
|
uint eip,
|
||
|
address token,
|
||
|
uint id,
|
||
|
uint amount
|
||
|
) public {
|
||
|
unchecked {
|
||
|
bytes32 key = keccak256(abi.encodePacked(sender, recipient, eip, token, id));
|
||
|
require(s_payments[key] >= amount, 'UniversalTokenRouter: INSUFFICIENT_ALLOWANCE');
|
||
|
s_payments[key] -= amount;
|
||
|
_transferToken(sender, recipient, eip, token, id, amount);
|
||
|
} }
|
||
|
|
||
|
function _transferToken(
|
||
|
address sender,
|
||
|
address recipient,
|
||
|
uint eip,
|
||
|
address token,
|
||
|
uint id,
|
||
|
uint amount
|
||
|
) internal {
|
||
|
if (eip == 20) {
|
||
|
if (sender == address(this)) {
|
||
|
TransferHelper.safeTransfer(token, recipient, amount);
|
||
|
} else {
|
||
|
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 if (eip == EIP_ETH) {
|
||
|
require(sender == address(this), 'UniversalTokenRouter: INVALID_ETH_SENDER');
|
||
|
TransferHelper.safeTransferETH(recipient, amount);
|
||
|
} else {
|
||
|
revert("UniversalTokenRouter: INVALID_EIP");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function _approve(
|
||
|
address recipient,
|
||
|
uint eip,
|
||
|
address token,
|
||
|
uint amount
|
||
|
) internal {
|
||
|
if (eip == 20) {
|
||
|
TransferHelper.safeApprove(token, recipient, amount);
|
||
|
} else if (eip == 1155) {
|
||
|
IERC1155(token).setApprovalForAll(recipient, amount > 0);
|
||
|
} else if (eip == 721) {
|
||
|
IERC721(token).setApprovalForAll(recipient, amount > 0);
|
||
|
} else {
|
||
|
revert("UniversalTokenRouter: INVALID_EIP");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function _balanceOf(
|
||
|
address owner,
|
||
|
uint eip,
|
||
|
address token,
|
||
|
uint id
|
||
|
) internal view returns (uint balance) {
|
||
|
if (eip == 20) {
|
||
|
return IERC20(token).balanceOf(owner);
|
||
|
}
|
||
|
if (eip == 1155) {
|
||
|
return IERC1155(token).balanceOf(owner, id);
|
||
|
}
|
||
|
if (eip == 721) {
|
||
|
if (id == ID_721_ALL) {
|
||
|
return IERC721(token).balanceOf(owner);
|
||
|
}
|
||
|
try IERC721(token).ownerOf(id) returns (address currentOwner) {
|
||
|
return currentOwner == owner ? 1 : 0;
|
||
|
} catch {
|
||
|
return 0;
|
||
|
}
|
||
|
}
|
||
|
if (eip == EIP_ETH) {
|
||
|
return owner.balance;
|
||
|
}
|
||
|
revert("UniversalTokenRouter: INVALID_EIP");
|
||
|
}
|
||
|
|
||
|
function _sliceUint(bytes memory bs, uint start) internal pure returns (uint x) {
|
||
|
// require(bs.length >= start + 32, "slicing out of range");
|
||
|
assembly {
|
||
|
x := mload(add(bs, start))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// https://github.com/GNSPS/solidity-bytes-utils/blob/master/contracts/BytesLib.sol
|
||
|
/// @param length length of the first preBytes
|
||
|
function _concat(
|
||
|
bytes memory preBytes,
|
||
|
uint length,
|
||
|
bytes memory postBytes
|
||
|
) internal pure returns (bytes memory bothBytes) {
|
||
|
assembly {
|
||
|
// Get a location of some free memory and store it in bothBytes as
|
||
|
// Solidity does for memory variables.
|
||
|
bothBytes := mload(0x40)
|
||
|
|
||
|
// Store the length of the first bytes array at the beginning of
|
||
|
// the memory for bothBytes.
|
||
|
mstore(bothBytes, length)
|
||
|
|
||
|
// Maintain a memory counter for the current write location in the
|
||
|
// temp bytes array by adding the 32 bytes for the array length to
|
||
|
// the starting location.
|
||
|
let mc := add(bothBytes, 0x20)
|
||
|
// Stop copying when the memory counter reaches the length of the
|
||
|
// first bytes array.
|
||
|
let end := add(mc, length)
|
||
|
|
||
|
for {
|
||
|
// Initialize a copy counter to the start of the preBytes data,
|
||
|
// 32 bytes into its memory.
|
||
|
let cc := add(preBytes, 0x20)
|
||
|
} lt(mc, end) {
|
||
|
// Increase both counters by 32 bytes each iteration.
|
||
|
mc := add(mc, 0x20)
|
||
|
cc := add(cc, 0x20)
|
||
|
} {
|
||
|
// Write the preBytes data into the bothBytes memory 32 bytes
|
||
|
// at a time.
|
||
|
mstore(mc, mload(cc))
|
||
|
}
|
||
|
|
||
|
// Add the length of postBytes to the current length of bothBytes
|
||
|
// and store it as the new length in the first 32 bytes of the
|
||
|
// bothBytes memory.
|
||
|
length := mload(postBytes)
|
||
|
mstore(bothBytes, add(length, mload(bothBytes)))
|
||
|
|
||
|
// Move the memory counter back from a multiple of 0x20 to the
|
||
|
// actual end of the preBytes data.
|
||
|
mc := sub(end, 0x20)
|
||
|
// Stop copying when the memory counter reaches the new combined
|
||
|
// length of the arrays.
|
||
|
end := add(end, length)
|
||
|
|
||
|
for {
|
||
|
let cc := postBytes
|
||
|
} lt(mc, end) {
|
||
|
mc := add(mc, 0x20)
|
||
|
cc := add(cc, 0x20)
|
||
|
} {
|
||
|
mstore(mc, mload(cc))
|
||
|
}
|
||
|
|
||
|
// Update the free-memory pointer by padding our last write location
|
||
|
// to 32 bytes: add 31 bytes to the end of bothBytes to move to the
|
||
|
// next 32 byte block, then round down to the nearest multiple of
|
||
|
// 32. If the sum of the length of the two arrays is zero then add
|
||
|
// one before rounding down to leave a blank 32 bytes (the length block with 0).
|
||
|
// mstore(0x40, and(
|
||
|
// add(add(end, iszero(add(length, mload(preBytes)))), 31),
|
||
|
// not(31) // Round down to the nearest 32 bytes.
|
||
|
// ))
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
```
|
||
|
|
||
|
## Security Considerations
|
||
|
|
||
|
`ACTION_INJECT_CALL_RESULT` SHOULD only be used for gas optimization, not as trusted conditions. Application contract code MUST always expect arbitruary, malformed or mallicious data can be passed in where the call result `bytes` is injected.
|
||
|
|
||
|
## Copyright
|
||
|
|
||
|
Copyright and related rights waived via [CC0](../LICENSE.md).
|