Skip to content

Commit

Permalink
Update EIP-6120: ERC-165 interface
Browse files Browse the repository at this point in the history
Merged by EIP-Bot.
  • Loading branch information
Zergity authored Oct 25, 2023
1 parent 0100f79 commit 2d796e8
Showing 1 changed file with 53 additions and 15 deletions.
68 changes: 53 additions & 15 deletions EIPS/eip-6120.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ status: Review
type: Standards Track
category: ERC
created: 2022-12-12
requires: 20, 721, 1014, 1155
requires: 20, 165, 721, 1014, 1155
---

## Abstract
Expand All @@ -21,7 +21,7 @@ The Universal Token Router (UTR) separates the token allowance from the applicat

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)), `amountIn`, `amountOutMin`, and `recipient`.

The Universal Token Router contract is deployed at `0x2222C5F0999E74D8D88F7bbfE300147d34c22222` using the [EIP-1014](./eip-1014.md) SingletonFactory contract `0xce0042B868300000d44A59004Da54A005ffdcf9f` with a salt of `2750646675` across all EVM-compatible networks. This allows new token contracts to pre-configure it as a trusted spender, eliminating the need for approval transactions during their interactive usage.
The Universal Token Router contract is deployed using the [EIP-1014](./eip-1014.md) SingletonFactory contract at a single address across all EVM-compatible networks. This enables new token contracts to pre-configure it as a trusted spender, eliminating the need for approval transactions during their interactive usage.

## Motivation

Expand Down Expand Up @@ -92,6 +92,22 @@ struct Action {
bytes data; // contract input data
}
```
The action code contract MUST implement the [ERC-165](./eip-165.md) interface with the ID `0x61206120` in order to be called by the UTR. This interface check prevents direct invocation of token *allowance-spending* functions (e.g., `transferFrom`) by the UTR. Therefore, new token contracts MUST NOT implement this interface ID.

```solidity
abstract contract NotToken is ERC165 {
// IERC165-supportsInterface
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
return
interfaceId == 0x61206120 ||
super.supportsInterface(interfaceId);
}
}
contract Application is NotToken {
// this contract can be used with the UTR
}
```

### Input

Expand Down Expand Up @@ -181,7 +197,7 @@ Please refer to the [Discard Payment](#discard-payment-1) section in the **Secur

##### 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 MUST be cleared before the `UTR.exec` ends.
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

Expand Down Expand Up @@ -412,6 +428,8 @@ Additional helper and adapter contracts might be needed, but they're mostly peri
A reference implementation by Derivable Labs, verified by Hacken.

```solidity
/// @title The implemetation of the EIP-6120.
/// @author Derivable Labs
contract UniversalTokenRouter is ERC165, IUniversalTokenRouter {
uint256 constant PAYMENT = 0;
uint256 constant TRANSFER = 1;
Expand All @@ -421,12 +439,15 @@ contract UniversalTokenRouter is ERC165, IUniversalTokenRouter {
uint256 constant ERC_721_BALANCE = uint256(keccak256('UniversalTokenRouter.ERC_721_BALANCE'));
// transient pending payments
/// @dev transient pending payments
mapping(bytes32 => uint256) t_payments;
// accepting ETH for user execution (e.g. WETH.withdraw)
/// @dev accepting ETH for user execution (e.g. WETH.withdraw)
receive() external payable {}
/// The main entry point of the router
/// @param outputs token behaviour for output verification
/// @param actions router actions and inputs for execution
function exec(
Output[] memory outputs,
Action[] memory actions
Expand All @@ -437,7 +458,7 @@ contract UniversalTokenRouter is ERC165, IUniversalTokenRouter {
Output memory output = outputs[i];
uint256 balance = _balanceOf(output);
uint256 expected = output.amountOutMin + balance;
require(expected >= balance, 'UniversalTokenRouter: OUTPUT_BALANCE_OVERFLOW');
require(expected >= balance, 'UTR: OUTPUT_BALANCE_OVERFLOW');
output.amountOutMin = expected;
}
Expand All @@ -459,24 +480,30 @@ contract UniversalTokenRouter is ERC165, IUniversalTokenRouter {
} else if (mode == TRANSFER) {
_transferToken(sender, input.recipient, input.eip, input.token, input.id, input.amountIn);
} else {
revert('UniversalTokenRouter: INVALID_MODE');
revert('UTR: INVALID_MODE');
}
}
}
if (action.data.length > 0) {
if (action.code != address(0) || action.data.length > 0 || value > 0) {
require(
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, allowances and left-overs
// 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.encodePacked(sender, input.recipient, input.eip, input.token, input.id));
bytes32 key = keccak256(abi.encodePacked(
sender, input.recipient, input.eip, input.token, input.id
));
delete t_payments[key];
}
}
Expand All @@ -493,10 +520,13 @@ contract UniversalTokenRouter is ERC165, IUniversalTokenRouter {
Output memory output = outputs[i];
uint256 balance = _balanceOf(output);
// NOTE: output.amountOutMin is reused as `expected`
require(balance >= output.amountOutMin, 'UniversalTokenRouter: INSUFFICIENT_OUTPUT_AMOUNT');
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);
(
Expand All @@ -509,16 +539,20 @@ contract UniversalTokenRouter is ERC165, IUniversalTokenRouter {
_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 transfering 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);
require(t_payments[key] >= amount, 'UniversalTokenRouter: INSUFFICIENT_PAYMENT');
require(t_payments[key] >= amount, 'UTR: INSUFFICIENT_PAYMENT');
unchecked {
t_payments[key] -= amount;
}
}
// IERC165-supportsInterface
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) {
function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
return
interfaceId == type(IUniversalTokenRouter).interfaceId ||
super.supportsInterface(interfaceId);
Expand All @@ -539,7 +573,7 @@ contract UniversalTokenRouter is ERC165, IUniversalTokenRouter {
} else if (eip == 721) {
IERC721(token).safeTransferFrom(sender, recipient, id);
} else {
revert("UniversalTokenRouter: INVALID_EIP");
revert("UTR: INVALID_EIP");
}
}
Expand All @@ -566,13 +600,17 @@ contract UniversalTokenRouter is ERC165, IUniversalTokenRouter {
if (eip == EIP_ETH) {
return output.recipient.balance;
}
revert("UniversalTokenRouter: INVALID_EIP");
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.
Expand Down

0 comments on commit 2d796e8

Please sign in to comment.