DCIPs/EIPS/eip-6120.md

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