Alert Source Discuss
⚠️ Review Standards Track: ERC

ERC-6120: Universal Token Router

A single router contract enables tokens to be sent to application contracts in the transfer-and-call manner instead of approve-then-call.

Authors Zergity (@Zergity), Ngo Quang Anh (@anhnq82), BerlinP (@BerlinP)
Created 2022-12-12
Requires EIP-20, EIP-721, EIP-1014, EIP-1155

Abstract

ETH is designed with transfer-and-call as the default behavior in a transaction. Unfortunately, EIP-20 is not designed with that pattern in mind and newer standards are too late to replace it as the de facto standard.

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 enormous amounts of user gas, waste network storage and throughput, and worsen user experience, but 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, EIP-20, EIP-721 or EIP-1155), amountInMax, amountOutMin, and recipient.

The Universal Token Router contract is counter-factually deployed using EIP-1014 at a single address across all EVM-compatible networks, so new token contracts can pre-configure it as a trusted spender and no approval transaction is necessary ever again.

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.

Application contracts follow this standard can use the Universal Token Router 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:

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

Output Verification

Output defines the expected token balance change for verification.

struct Output {
    address recipient;
    uint eip;           // token standard: 0 for ETH or EIP number
    address token;      // token contract address
    uint id;            // token id for EIP-721 and EIP-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 EIP-721, which can be used in output actions to verify the total amount of all ids owned by the recipient address.

ID_721_ALL = keccak256('UniversalTokenRouter.ID_721_ALL')

Action

Action defines the token inputs and the contract call.

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.

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 EIP721 and EIP1155
    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.)

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:

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:

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:

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.

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:

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:

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:

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:

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:

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:

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:

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 (EIP-20, EIP-721 and EIP-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 (EIP-20, EIP-721, and EIP-1155) to msg.sender can use the UTR output token transfer sub-action to re-direct tokens to another recipient address.

// 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,
        eip: 20,
        token: WETH.address,
        id: 0,
        amountInMax: 0,             // no limit
        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.

// 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

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

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

Zergity (@Zergity), Ngo Quang Anh (@anhnq82), BerlinP (@BerlinP), "EIP-6120: Universal Token Router [DRAFT]," Ethereum Improvement Proposals, no. 6120, December 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-6120.