30 KiB
eip | title | description | author | discussions-to | status | type | category | created | requires |
---|---|---|---|---|---|---|---|---|---|
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. | Zergity (@Zergity), Ngo Quang Anh (@anhnq82), BerlinP (@BerlinP), Khanh Pham (@blackskin18) | https://ethereum-magicians.org/t/eip-6120-universal-token-router/12142 | Review | Standards Track | ERC | 2022-12-12 | 20, 721, 1014, 1155 |
Abstract
ETH is designed with transfer-and-call as the default behavior in a transaction. Unfortunately, ERC-20 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, ERC-721 or ERC-1155), amountInMax
, amountOutMin
, and recipient
.
The Universal Token Router contract is counter-factually deployed using EIP-1014 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
orecrecover
) - 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:
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 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.
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 abytes
for subsequent actions.0x4 = ACTION_INJECT_CALL_RESULT
: the last call resultbytes
recorded will be injected to the last emptybytes
param of the contract functiondata
.
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 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 frommsg.sender
torecipient
.1 = TRANSFER_FROM_ROUTER
: the token will be transferred fromthis
UTR contract torecipient
.2 = TRANSFER_CALL_VALUE
: the token amount will be passed to the action as the callvalue
.4 = IN_TX_PAYMENT
: the token will be allowed to be spent in this transaction by callingUTR.pay
.8 = ALLOWANCE_BRIDGE
: the token will be transferred frommsg.sender
tothis
UTR contract and is allowed to be spent in this transaction.
amountSource
defines how the actual token amountIn
is acquired from:
0 = AMOUNT_EXACT
: theamountInMax
value is used.1 = AMOUNT_ALL
: the entire balance of the sender (msg.sender
orthis
) is used.- otherwise, extracts the
uint256
value starting from theamountSource
-th byte of the last recorded call resultbytes
. This value is unpredictable if there's no prior action with theACTION_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 frommsg.sender
tothis
UTR contract. - the
recipient
address is allowed to spend the token fromthis
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 (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.
// 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.
// 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 injected.
Copyright
Copyright and related rights waived via CC0.