Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
2563ede
Golf AllowanceHolder calldata decoding
duncancmt Oct 10, 2024
927c216
Add comment about technically not-memory-safe code (but it is actuall…
duncancmt Oct 10, 2024
71113e7
Use `return` instead of `revert` inside `balanceOf` to make transacti…
duncancmt Oct 10, 2024
271df28
Remove EIP-3074 (EIP-7702) footgun from AllowanceHolder (homogenizes …
duncancmt Oct 10, 2024
6fc2989
Comments
duncancmt Oct 10, 2024
65d5088
Function visibility
duncancmt Oct 10, 2024
e68e4a7
Reorder constant arguments in Yul
duncancmt Oct 10, 2024
e8ab289
Apply the all-but-one-64th rule 3 times (not 2)
duncancmt Oct 10, 2024
9b60623
Bug! The first argument word is the 4 bytes into calldata
duncancmt Oct 13, 2024
d349d18
Typo
duncancmt Oct 13, 2024
ecb00c9
Implement ABIEncoding in assembly
duncancmt Oct 15, 2024
12c4388
Implement reverting in assembly
duncancmt Oct 15, 2024
39e5224
Return strict ABIEncoding of `result`
duncancmt Oct 15, 2024
1444d8c
Update snaps
duncancmt Oct 16, 2024
635baa0
Remove Solmate `SafeTransferLib`; port AllowanceHolder to the Solady …
duncancmt Oct 17, 2024
2beee66
Update snaps
duncancmt Oct 17, 2024
2092489
Merge branch 'master' into dcmt/allowanceholder2
duncancmt Oct 30, 2024
7d63a4f
Use technique from `fastBalanceOf` in `AllowanceHolderBase`'s `_rejec…
duncancmt Oct 30, 2024
ce5c565
Update snaps
duncancmt Oct 30, 2024
935a7c4
Golf
duncancmt Jan 24, 2025
38f70dd
Update snaps
duncancmt Jan 24, 2025
c5994c8
Merge branch 'master' into dcmt/allowanceholder2
duncancmt Mar 4, 2025
f28d955
Adopt the same pattern as in `MultiCall` where the ERC2771 forwarder …
duncancmt Mar 4, 2025
e3daeec
Solc 0.8.28
duncancmt Mar 4, 2025
62babd7
Merge branch 'master' into dcmt/allowanceholder2
duncancmt Nov 24, 2025
d550fb4
Add `MultiCallContext` to `AllowanceHolder`
duncancmt Nov 24, 2025
6dbf06d
Golf
duncancmt Nov 24, 2025
01f8003
Comment, formatting
duncancmt Nov 24, 2025
270e598
Golf
duncancmt Dec 4, 2025
5997305
Golf
duncancmt Dec 4, 2025
a5e923d
Simplify
duncancmt Dec 4, 2025
79203c4
Golf
duncancmt Dec 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .forge-snapshots/allowanceHolder_rfq_DAI-WETH.snap

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .forge-snapshots/allowanceHolder_rfq_USDC-USDT.snap

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .forge-snapshots/allowanceHolder_rfq_USDC-WETH.snap

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .forge-snapshots/allowanceHolder_rfq_USDT-WETH.snap

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .forge-snapshots/settler_curveTricrypto_USDC-WETH.snap

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .forge-snapshots/settler_curveTricrypto_USDT-WETH.snap

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .forge-snapshots/settler_rfq_DAI-USDC.snap

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .forge-snapshots/settler_rfq_DAI-WETH.snap

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .forge-snapshots/settler_rfq_USDC-DAI.snap

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .forge-snapshots/settler_rfq_USDC-USDT.snap

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .forge-snapshots/settler_rfq_USDC-WETH.snap

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .forge-snapshots/settler_rfq_USDT-WETH.snap

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -723,7 +723,7 @@ comparison.

| Curve | DEX | Pair | Gas | % |
| ----------------- | --------------------- | --------- | ------ | ------- |
| Settler | CurveV2 Tricrypto VIP | USDC/WETH | 231504 | NaN% |
| Settler | CurveV2 Tricrypto VIP | USDC/WETH | 231583 | NaN% |
| | | | | |
| | | | | |
| 0x V4 | Curve | USDT/WETH | 400419 | 0.00% |
Expand Down
19 changes: 4 additions & 15 deletions src/allowanceholder/AllowanceHolder.sol
Original file line number Diff line number Diff line change
@@ -1,26 +1,15 @@
// SPDX-License-Identifier: MIT
pragma solidity =0.8.25;
pragma solidity =0.8.28;

import {AllowanceHolderBase} from "./AllowanceHolderBase.sol";
import {TransientStorage} from "./TransientStorage.sol";

/// @custom:security-contact security@0x.org
contract AllowanceHolder is TransientStorage, AllowanceHolderBase {
constructor() {
require(address(this) == 0x0000000000001fF3684f28c67538d4D072C22734 || block.chainid == 31337);
}

/// @inheritdoc AllowanceHolderBase
function exec(address operator, address token, uint256 amount, address payable target, bytes calldata data)
internal
override
returns (bytes memory)
{
(bytes memory result, address sender, TSlot allowance) = _exec(operator, token, amount, target, data);
// EIP-3074 seems unlikely
if (sender != tx.origin) {
_set(allowance, 0);
// Check that we're on a chain with transient storage support
assembly ("memory-safe") {
tstore(0x00, 0x00)
}
return result;
}
}
179 changes: 121 additions & 58 deletions src/allowanceholder/AllowanceHolderBase.sol
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;
pragma solidity =0.8.28;

import {IAllowanceHolder} from "./IAllowanceHolder.sol";
import {IERC20} from "@forge-std/interfaces/IERC20.sol";
import {SafeTransferLib} from "../vendor/SafeTransferLib_Solmate.sol";
import {SafeTransferLib} from "../vendor/SafeTransferLib.sol";
import {CheckCall} from "../utils/CheckCall.sol";
import {FreeMemory} from "../utils/FreeMemory.sol";
import {Panic} from "../utils/Panic.sol";
import {Ternary} from "../utils/Ternary.sol";
import {TransientStorageLayout} from "./TransientStorageLayout.sol";

/// @notice Thrown when validating the target, avoiding executing against an ERC20 directly
Expand All @@ -14,98 +16,146 @@ error ConfusedDeputy();
abstract contract AllowanceHolderBase is TransientStorageLayout, FreeMemory {
using SafeTransferLib for IERC20;
using CheckCall for address payable;
using Ternary for bool;

address internal constant _MULTICALL = 0x00000000000000CF9E3c5A26621af382fA17f24f;

constructor() {
assert(
(msg.sender == 0x4e59b44847b379578588920cA78FbF26c0B4956C && uint160(address(this)) >> 104 == 0)
|| block.chainid == 31337
);
}

function _rejectIfERC20(address payable maybeERC20, bytes calldata data) private view DANGEROUS_freeMemory {
// We could just choose a random address for this check, but to make
// confused deputy attacks harder for tokens that might be badly behaved
// (e.g. tokens with blacklists), we choose to copy the first argument
// out of `data` and mask it as an address. If there isn't enough
// `data`, we use 0xdead instead.
address target;
if (data.length > 0x10) {
target = address(uint160(bytes20(data[0x10:])));

address target; // = address(uint160(bytes20(data[0x10:])));
assembly ("memory-safe") {
target := calldataload(add(0x04, data.offset))
// `shl(0x08, data.length)` can't overflow because we're going to
// `calldatacopy(..., data.length)` later. It would OOG. We check
// for underflow in `sub(data.length, 0x04)` later.
let mask := shr(shl(0x08, sub(data.length, 0x04)), not(0x00))
// Zero the low bits of `target` if `data` is short. Dirty low bits
// are only ever possible with nonstandard encodings, like ERC-2771.
target := and(not(mask), target)
// Zero `target` if `sub(data.length, 0x04)` underflowed.
target := mul(lt(0x03, data.length), target)
}
// EIP-1352 (not adopted) specifies 0xffff as the maximum precompile
if (target <= address(0xffff)) {
// 0xdead is a conventional burn address; we assume that it is not treated specially
target = address(0xdead);

// EIP-1352 (not adopted) specifies 0xffff as the maximum precompile.
// 0xdead is a conventional burn address; we assume that it is not
// treated specially.
target = (target > address(0xffff)).ternary(target, address(0xdead));

bytes memory testData; // = abi.encodeCall(IERC20.balanceOf, target);
assembly ("memory-safe") {
testData := mload(0x40)
mstore(add(0x24, testData), target)
mstore(add(0x10, testData), 0x70a08231000000000000000000000000) // `IERC20.balanceOf.selector` with `target`'s padding
mstore(testData, 0x24)
mstore(0x40, add(0x60, testData))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you doing 0x60 to keep the memory alignment?

}
bytes memory testData = abi.encodeCall(IERC20.balanceOf, target);
if (maybeERC20.checkCall(testData, 0x20)) revert ConfusedDeputy();
}

function _msgSender() private view returns (address sender) {
if ((sender = msg.sender) == address(this)) {
if (maybeERC20.checkCall(testData, 0x20)) {
assembly ("memory-safe") {
sender := shr(0x60, calldataload(sub(calldatasize(), 0x14)))
mstore(0x00, 0xe758b8d5) // Selector for `ConfusedDeputy()`
revert(0x1c, 0x04)
}
}
}

/// @dev This virtual function provides the implementation for the function
/// of the same name in `IAllowanceHolder`. It is unimplemented in this
/// base contract to accommodate the customization required to support
/// both chains that have EIP-1153 (transient storage) and those that
/// don't.
function _msgSender() private view returns (address sender) {
assembly ("memory-safe") {
let isSelfForwarded := eq(caller(), address())
let isMultiCallForwarded := and(lt(0x03, calldatasize()), eq(_MULTICALL, caller()))
sender :=
xor(
caller(),
mul(
xor(caller(), shr(0x60, calldataload(sub(calldatasize(), 0x14)))),
or(isMultiCallForwarded, isSelfForwarded)
)
)
}
}

/// @dev This function provides the implementation for the function of the
/// same name in `IAllowanceHolder`. The arguments and return value
/// have the same meaning as documented there.
function exec(address operator, address token, uint256 amount, address payable target, bytes calldata data)
internal
virtual
returns (bytes memory result);

/// @dev This is the majority of the implementation of IAllowanceHolder.exec
/// . The arguments have the same meaning as documented there.
/// @return result
/// @return sender The (possibly forwarded) message sender that is
/// requesting the allowance be set. Provided to avoid
/// duplicated computation in customized `exec`
/// @return allowance The slot where the ephemeral allowance is
/// stored. Provided to avoid duplicated computation in
/// customized `exec`
function _exec(address operator, address token, uint256 amount, address payable target, bytes calldata data)
internal
returns (bytes memory result, address sender, TSlot allowance)
private
returns (bytes memory result)
{
// This contract has no special privileges, except for the allowances it
// holds. In order to prevent abusing those allowances, we prohibit
// sending arbitrary calldata (doing `target.call(data)`) to any
// contract that might be an ERC20.
_rejectIfERC20(target, data);

sender = _msgSender();
allowance = _ephemeralAllowance(operator, sender, token);
_set(allowance, amount);
address sender = _msgSender();
TSlot allowanceSlot = _ephemeralAllowance(operator, sender, token);
_set(allowanceSlot, amount);

// For gas efficiency we're omitting a bunch of checks here. Notably,
// we're omitting the check that `address(this)` has sufficient value to
// send (we know it does), and we're omitting the check that `target`
// contains code (we already checked in `_rejectIfERC20`).
assembly ("memory-safe") {
// Copy the payload from calldata into memory
result := mload(0x40)
calldatacopy(result, data.offset, data.length)
// ERC-2771 style msgSender forwarding https://eips.ethereum.org/EIPS/eip-2771

// ERC-2771 style `msgSender` forwarding https://eips.ethereum.org/EIPS/eip-2771
// We do not append the forwarded sender if the payload has no selector
mstore(add(result, data.length), shl(0x60, sender))
let success := call(gas(), target, callvalue(), result, add(data.length, 0x14), 0x00, 0x00)
let ptr := add(result, 0x20)
let length := add(mul(0x14, lt(0x03, data.length)), data.length)

// Perform the call
let success := call(gas(), target, callvalue(), result, length, 0x00, 0x00)

// Copy returndata into memory; if it is a revert, bubble
let ptr := add(0x20, result)
returndatacopy(ptr, 0x00, returndatasize())
switch success
case 0 { revert(ptr, returndatasize()) }
default {
// Wrap the returndata in a level of ABIEncoding
mstore(result, returndatasize())
mstore(0x40, add(ptr, returndatasize()))
mstore(0x40, add(returndatasize(), ptr))
}
}

_set(allowanceSlot, 0);
}

/// @dev This provides the implementation of the function of the same name
/// in `IAllowanceHolder`.
function transferFrom(address token, address owner, address recipient, uint256 amount) internal {
// msg.sender is the assumed and later validated operator
TSlot allowance = _ephemeralAllowance(msg.sender, owner, token);
// validation of the ephemeral allowance for operator, owner, token via
// uint underflow
_set(allowance, _get(allowance) - amount);
/// in `IAllowanceHolder`. The arguments have the same meaning as
/// documented there.
function transferFrom(address token, address owner, address recipient, uint256 amount) private {
// `msg.sender` is the assumed and later validated `operator`.
TSlot allowanceSlot = _ephemeralAllowance(msg.sender, owner, token);
uint256 allowanceValue = _get(allowanceSlot);

// We validate the ephemeral allowance for the 3-tuple of `operator`
// (`msg.sender`), `owner`, and `token` by reverting on unsigned integer
// underflow.
if (allowanceValue < amount) {
Panic.panic(Panic.ARITHMETIC_OVERFLOW);
}

// Update the ephemeral allowance
unchecked {
_set(allowanceSlot, allowanceValue - amount);
}

// `safeTransferFrom` does not check that `token` actually contains
// code. It is the responsibility of integrating code to check for that
// code. It is the responsibility of integrating code to check for that,
// if vacuous success is a security concern.
IERC20(token).safeTransferFrom(owner, recipient, amount);
}
Expand Down Expand Up @@ -136,7 +186,7 @@ abstract contract AllowanceHolderBase is TransientStorageLayout, FreeMemory {

transferFrom(token, owner, recipient, amount);

// return true;
// `return true;`
assembly ("memory-safe") {
mstore(0x00, 0x01)
return(0x00, 0x20)
Expand Down Expand Up @@ -171,20 +221,33 @@ abstract contract AllowanceHolderBase is TransientStorageLayout, FreeMemory {

bytes memory result = exec(operator, token, amount, target, data);

// return result;
// `return result;`
assembly ("memory-safe") {
let returndata := sub(result, 0x20)
// This is technically not "memory-safe", but manual examination
// of the compiled bytecode shows that it's OK.
mstore(returndata, 0x20)
return(returndata, add(0x40, mload(result)))

// Pad `returndata` to a multiple of 32 bytes.
let len := mload(result)
let m := and(0x1f, len)
if m {
mstore(add(add(0x20, result), len), 0x00)
len := add(sub(0x20, m), len)
}
Comment on lines +231 to +237
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what made you add this padding here?


// Return the ABIEncoding of `result`.
return(returndata, add(0x40, len))
}
} else if (selector == uint256(uint32(IERC20.balanceOf.selector))) {
// balanceOf(address) reverts with a single byte of returndata,
// making it more gas efficient to pass the `_rejectERC20` check
// `balanceOf(address)` returns a single byte of returndata, making
// it more gas efficient to pass the `_rejectERC20` check during
// recursive/reentrant calls.
assembly ("memory-safe") {
revert(0x00, 0x01)
return(0x00, 0x01)
}
} else {
// emulate standard Solidity behavior
// Emulate standard Solidity behavior.
assembly ("memory-safe") {
revert(0x00, 0x00)
}
Expand Down
Loading