From 0805e736d6e8d3bbe7db1be1850eb5f1bccfaad0 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Sat, 8 Nov 2025 11:52:10 +0100 Subject: [PATCH 01/72] Golf CrossChainReceiverFactory proxy initcode (compute at deploy time) --- src/CrossChainReceiverFactory.sol | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index cb03b2821..967a4b54b 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -31,6 +31,8 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte CrossChainReceiverFactory private immutable _cachedThis = this; uint168 private immutable _factoryWithFF = 0xff0000000000000000000000000000000000000000 | uint168(uint160(address(this))); + bytes32 private immutable _proxyInitCode0 = bytes32(bytes20(0x60253d8160093d39f33d3d3d3d363d3d37363d6c)) | bytes32(uint256(uint160(address(this))) >> 8); + bytes32 private immutable _proxyInitCode1 = bytes32(bytes1(uint8(uint160(address(this))))) | bytes32(0x5af43d3d93803e602357fd5bf3 << 144); bytes32 private immutable _proxyInitHash = keccak256( bytes.concat( hex"60253d8160093d39f33d3d3d3d363d3d37363d6c", @@ -38,9 +40,12 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte hex"5af43d3d93803e602357fd5bf3" ) ); + string public constant override name = "ZeroExCrossChainReceiver"; bytes32 private constant _NAMEHASH = 0x819c7f86c24229cd5fed5a41696eb0cd8b3f84cc632df73cfd985e8b100980e8; + IERC20 private constant _NATIVE = IERC20(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); + address private constant _TOEHOLD = 0x4e59b44847b379578588920cA78FbF26c0B4956C; address private constant _WNATIVE_SETTER = 0x000000000000F01B1D1c8EEF6c6cF71a0b658Fbc; bytes32 private constant _WNATIVE_STORAGE_INITHASH = keccak256( @@ -288,6 +293,8 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte noDelegateCall returns (ICrossChainReceiverFactory proxy) { + bytes32 proxyInitCode0 = _proxyInitCode0; + bytes32 proxyInitCode1 = _proxyInitCode1; assembly ("memory-safe") { // derive the deployment salt from the owner mstore(0x14, initialOwner) @@ -295,10 +302,9 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte let salt := keccak256(returndatasize(), 0x34) // create a minimal proxy targeting this contract - mstore(0x1a, 0x5af43d3d93803e602357fd5bf3) - mstore(0x0d, address()) - mstore(returndatasize(), 0x60253d8160093d39f33d3d3d3d363d3d37363d6c) - proxy := create2(returndatasize(), 0x0c, 0x2e, salt) + mstore(returndatasize(), proxyInitCode0) + mstore(0x20, proxyInitCode1) + proxy := create2(returndatasize(), returndatasize(), 0x2e, salt) if iszero(proxy) { mstore(returndatasize(), 0x30116425) // selector for `DeploymentFailed()`. revert(0x1c, 0x04) From 73ee172a6332dfd2f6f6eafdff367a97ac88bb16 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Wed, 26 Nov 2025 13:51:44 +0100 Subject: [PATCH 02/72] First draft of improved, more-composable CrossChainReceiverFactory --- src/CrossChainReceiverFactory.sol | 211 +++++++++++++++++++++++++----- 1 file changed, 180 insertions(+), 31 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index 967a4b54b..6c07552a3 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -7,9 +7,11 @@ import {IERC1271} from "./interfaces/IERC1271.sol"; import {IERC5267} from "./interfaces/IERC5267.sol"; import {IOwnable} from "./interfaces/IOwnable.sol"; +import {ISignatureTransfer} from "@permit2/interfaces/ISignatureTransfer.sol"; + import {ICrossChainReceiverFactory} from "./interfaces/ICrossChainReceiverFactory.sol"; import {AbstractOwnable, TwoStepOwnable} from "./utils/TwoStepOwnable.sol"; -import {MultiCallContext, MULTICALL_ADDRESS} from "./multicall/MultiCallContext.sol"; +import {IMultiCall, MultiCallContext, MULTICALL_ADDRESS} from "./multicall/MultiCallContext.sol"; import {FastLogic} from "./utils/FastLogic.sol"; import {Ternary} from "./utils/Ternary.sol"; @@ -31,8 +33,10 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte CrossChainReceiverFactory private immutable _cachedThis = this; uint168 private immutable _factoryWithFF = 0xff0000000000000000000000000000000000000000 | uint168(uint160(address(this))); - bytes32 private immutable _proxyInitCode0 = bytes32(bytes20(0x60253d8160093d39f33d3d3d3d363d3d37363d6c)) | bytes32(uint256(uint160(address(this))) >> 8); - bytes32 private immutable _proxyInitCode1 = bytes32(bytes1(uint8(uint160(address(this))))) | bytes32(0x5af43d3d93803e602357fd5bf3 << 144); + bytes32 private immutable _proxyInitCode0 = + bytes32(bytes20(0x60253d8160093d39F33d3d3D3D363D3D37363d6C)) | bytes32(uint256(uint160(address(this))) >> 8); + bytes32 private immutable _proxyInitCode1 = + bytes32(bytes1(uint8(uint160(address(this))))) | bytes32(0x5af43d3d93803e602357fd5bf3 << 144); bytes32 private immutable _proxyInitHash = keccak256( bytes.concat( hex"60253d8160093d39f33d3d3d3d363d3d37363d6c", @@ -43,6 +47,10 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte string public constant override name = "ZeroExCrossChainReceiver"; bytes32 private constant _NAMEHASH = 0x819c7f86c24229cd5fed5a41696eb0cd8b3f84cc632df73cfd985e8b100980e8; + bytes32 private constant _DOMAIN_TYPEHASH = 0x8cad95687ba82c2ce50e74f7b754645e5117c3a5bec8151c0726d5857980a866; + + // TODO: should the `MultiCall` EIP712 type include an (optional) relayer field that binds the metatransaction to a specific relayer? + bytes32 private constant _MULTICALL_TYPEHASH = 0xd0290069becb7f8c7bc360deb286fb78314d4fb3e65d17004248ee046bd770a9; IERC20 private constant _NATIVE = IERC20(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); @@ -88,8 +96,13 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte IWrappedNative private immutable _WNATIVE = IWrappedNative(payable(address(uint160(uint256(bytes32(_WNATIVE_STORAGE.code)))))); + ISignatureTransfer private constant _PERMIT2 = ISignatureTransfer(0x000000000022D473030F116dDEE9F6B43aC78BA3); + error DeploymentFailed(); error ApproveFailed(); + error InvalidNonce(); + error InvalidSigner(); + error SignatureExpired(uint256 deadline); constructor() payable { // This bit of bizarre functionality is required to accommodate Foundry's `deployCodeTo` @@ -103,6 +116,12 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte require(((msg.sender == _TOEHOLD).and(uint160(address(this)) >> 104 == 0)).or(block.chainid == 31337)); require(uint160(_WNATIVE_SETTER) >> 112 == 0); require(_NAMEHASH == keccak256(bytes(name))); + require(_DOMAIN_TYPEHASH == keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)")); + require( + _MULTICALL_TYPEHASH = keccak256( + "MultiCall(Call[] calls,uint256 contextdepth,uint256 nonce,uint256 deadline)Call(address target,uint8 revertPolicy,uint256 value,bytes data)" + ) + ); // do some behavioral checks on `_WNATIVE` { @@ -152,6 +171,14 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte _; } + modifier onlyOwnerOrSelf() { + address msgSender = _msgSender(); + if (msgSender != address(this) && msgSender != owner()) { + _permissionDenied(); + } + _; + } + /// @inheritdoc IERC165 function supportsInterface(bytes4 interfaceId) public @@ -227,32 +254,8 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte } if (validOwner) { - assembly ("memory-safe") { - // This assembly block is equivalent to: - // hash = keccak256(abi.encode(hash, block.chainid)); - // except that it's cheaper and doesn't allocate memory. We make the assumption - // here that `block.chainid` cannot alias a valid tree node or signing - // hash. Realistically, `block.chainid` cannot exceed 2**53 - 1 or it would - // cause significant issues elsewhere in the ecosystem. This also means that the - // sort order of the hash and the chainid is backwards from what - // `_getMerkleRoot` produces, again protecting us against extension attacks. - mstore(returndatasize(), hash) - mstore(0x20, chainid()) - hash := keccak256(returndatasize(), 0x40) - } - - bytes32[] calldata proof; - assembly ("memory-safe") { - // This assembly block simply ABIDecodes `proof` as the second element of the - // encoded anonymous struct `(owner, proof)`. It omits range and overflow - // checking. - // (, proof) = abi.decode(signature, (address, bytes32[])); - proof.offset := add(signature.offset, calldataload(add(0x20, signature.offset))) - proof.length := calldataload(proof.offset) - proof.offset := add(0x20, proof.offset) - } - - return _verifyDeploymentRootHash(_getMerkleRoot(proof, hash), originalOwner).ternary( + (bytes32 leafHash, bytes32[] calldata proof) = _formatMerkleProof(hash, signature); + return _verifyDeploymentRootHash(_getMerkleRoot(proof, leafHash), originalOwner).ternary( IERC1271.isValidSignature.selector, bytes4(0xffffffff) ); } @@ -264,6 +267,36 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte ); } + function _formatMerkleProof(bytes32 hash, bytes calldata signature) + private + view + returns (bytes32 leafHash, bytes32[] calldata proof) + { + assembly ("memory-safe") { + // This assembly block is equivalent to: + // hash = keccak256(abi.encode(hash, block.chainid)); + // except that it's cheaper and doesn't allocate memory. We make the assumption here + // that `block.chainid` cannot alias a valid tree node or signing hash. Realistically, + // `block.chainid` cannot exceed 2**53 - 1 or it would cause significant issues + // elsewhere in the ecosystem. This also means that the sort order of the hash and the + // chainid is backwards from what `_getMerkleRoot` produces, again protecting us against + // extension attacks. + mstore(returndatasize(), hash) + mstore(0x20, chainid()) + leafHash := keccak256(returndatasize(), 0x40) + } + + bytes32[] calldata proof; + assembly ("memory-safe") { + // This assembly block simply ABIDecodes `proof` as the second element of the encoded + // anonymous struct `(owner, proof)`. It omits range and overflow checking. + // (, proof) = abi.decode(signature, (address, bytes32[])); + proof.offset := add(signature.offset, calldataload(add(0x20, signature.offset))) + proof.length := calldataload(proof.offset) + proof.offset := add(0x20, proof.offset) + } + } + /// @inheritdoc IERC5267 function eip712Domain() external @@ -350,7 +383,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte let ptr := mload(0x40) mstore(returndatasize(), 0x095ea7b3) // selector for `approve(address,uint256)` - mstore(0x20, 0x000000000022D473030F116dDEE9F6B43aC78BA3) // Permit2 + mstore(0x20, _PERMIT2) mstore(0x40, amount) if iszero(call(gas(), token, callvalue(), 0x1c, 0x44, returndatasize(), 0x20)) { @@ -372,7 +405,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte function call(address payable target, uint256 value, bytes calldata data) external override - onlyOwner + onlyOwnerOrSelf returns (bytes memory) { assembly ("memory-safe") { @@ -391,6 +424,122 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte } } + function call(address payable target, IERC20 token, uint256 patchOffset, bytes calldata data) + external + override + onlyOwnerOrSelf + { + // TODO: + } + + function _useUnorderedNonce(uint256 nonce) private { + assembly ("memory-safe") { + let ptr := mload(0x40) + let wordPos := shr(0x08, nonce) + let bitPos := shl(0x01, and(0xff, nonce)) + mstore(0x00, 0x4fe02b44) // `ISignatureTransfer.nonceBitmap.selector` + mstore(0x20, address()) + mstore(0x40, wordPos) + if iszero(staticcall(gas(), _PERMIT2, 0x1c, 0x44, 0x20, 0x20)) { + returndatacopy(ptr, 0x00, returndatasize()) + revert(ptr, returndatasize()) + } + let canceledNonces := mload(returndatasize()) + if xor(canceledNonces, bitPos) { + mstore(returndatasize(), 0x756688fe) // `InvalidNonce.selector` + revert(0x3c, 0x04) + } + mstore(0x00, 0x3ff9dcb1) // `ISignatureTransfer.invalidateUnorderedNonces.selector` + mstore(returndatasize(), wordPos) + mstore(0x40, bitPos) + if iszero(call(gas(), _PERMIT2, 0x00, 0x1c, 0x44, codesize(), 0x00)) { + returndatacopy(ptr, 0x00, returndatasize()) + revert(ptr, returndatasize()) + } + mstore(0x40, ptr) + } + } + + function _verifySimpleSignature(bytes32 signingHash, bytes calldata rsv, address owner_) private view { + assembly ("memory-safe") { + let ptr := mload(0x40) + mstore(0x00, signingHash) + calldatacopy(0x20, rsv.offset, 0x41) + mstore(0x60, shr(0xf8, mload(0x60))) + let recovered := mload(staticcall(gas(), 0x01, 0x00, 0x80, 0x01, 0x20)) + if shl(0x60, xor(owner_, recovered)) { + mstore(0x00, 0x815e1d64) // `InvalidSigner.selector` + revert(0x1c, 0x04) + } + mstore(0x40, ptr) + mstore(0x60, 0x00) + } + } + + function _hashEip712(bytes32 structHash) private pure returns (bytes32 signingHash) { + assembly ("memory-safe") { + let ptr := mload(0x40) + mstore(0x00, _DOMAIN_TYPEHASH) + mstore(0x20, _NAMEHASH) + mstore(0x40, chainid()) + mstore(0x60, address()) + let domain := keccak256(0x00, 0x80) + mstore(0x00, 0x1901) + mstore(0x20, domain) + mstore(0x40, structHash) + signingHash := keccak256(0x1e, 0x42) + mstore(0x40, ptr) + mstore(0x60, 0x00) + } + } + + function _hashMultiCall(IMultiCall.Call[] calldata calls, uint256 contextdepth, uint256 nonce, uint256 deadline) + private + pure + returns (bytes32 structHash) + { + // TODO: + } + + function metaTx( + IMultiCall.Call[] calldata calls, + uint256 contextdepth, + uint256 nonce, + uint256 deadline, + bytes calldata signature + ) external override onlyProxy returns (IMultiCall.Result[] memory) { + if (block.timestamp > deadline) { + assembly ("memory-safe") { + mstore(returndatasize(), 0xcd21db4f) // `SignatureExpired.selector` + mstore(0x20, deadline) + revert(0x1c, 0x24) + } + } + _useUnorderedNonce(nonce); + bytes32 signingHash = _hashEip712(_hashMultiCall(calls, contextdepth, nonce, deadline)); + + address owner_; + bool validOwner; + assembly ("memory-safe") { + owner_ := calldataload(signature.offset) + validOwner := iszero(shr(0xa0, owner_)) + } + if (validOwner) { + (bytes32 leafHash, bytes32[] calldata proof) = _formatMerkleProof(signingHash, signature); + if (!_verifyDeploymentRootHash(_getMerkleRoot(proof, leafHash), owner_)) { + assembly ("memory-safe") { + mstore(0x00, 0x815e1d64) // `InvalidSigner.selector` + revert(0x1c, 0x04) + } + } + } else { + owner_ = super.owner(); + _verifySimpleSignature(signingHash, signature, owner_); + } + + return IMultiCall(MULTICALL_ADDRESS).multicall(calls, contextdepth); + } + /// @inheritdoc ICrossChainReceiverFactory function cleanup(address payable beneficiary) external override { if (msg.sender == address(_cachedThis)) { From 38f2f562c0e145fc8563b31103862bb153b286ba Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Wed, 26 Nov 2025 13:56:16 +0100 Subject: [PATCH 03/72] Do some cleanup; add some TODOs --- src/CrossChainReceiverFactory.sol | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index 6c07552a3..62064da1c 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -281,9 +281,9 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte // elsewhere in the ecosystem. This also means that the sort order of the hash and the // chainid is backwards from what `_getMerkleRoot` produces, again protecting us against // extension attacks. - mstore(returndatasize(), hash) + mstore(0x00, hash) mstore(0x20, chainid()) - leafHash := keccak256(returndatasize(), 0x40) + leafHash := keccak256(0x00, 0x40) } bytes32[] calldata proof; @@ -508,6 +508,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte uint256 deadline, bytes calldata signature ) external override onlyProxy returns (IMultiCall.Result[] memory) { + // TODO: make this more checks-effects-interactions if (block.timestamp > deadline) { assembly ("memory-safe") { mstore(returndatasize(), 0xcd21db4f) // `SignatureExpired.selector` @@ -587,7 +588,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte mstore(xor(0x20, leafSlot), calldataload(offset)) // Reuse leaf to store the hash to reduce stack operations. - leaf := keccak256(returndatasize(), 0x40) // Hash both slots of scratch space. + leaf := keccak256(0x00, 0x40) // Hash both slots of scratch space. offset := add(0x20, offset) // Shift 1 word per cycle. @@ -606,13 +607,13 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte // derive creation salt mstore(0x14, originalOwner) - mstore(returndatasize(), root) - let salt := keccak256(returndatasize(), 0x34) + mstore(0x00, root) + let salt := keccak256(0x00, 0x34) // 0xff + factory + salt + hash(initCode) mstore(0x40, initHash) mstore(0x20, salt) - mstore(returndatasize(), factoryWithFF) + mstore(0x00, factoryWithFF) let computedAddress := keccak256(0x0b, 0x55) // restore clobbered memory From e084d6f1e37262664bcb519b15e981cf94e67005 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Wed, 26 Nov 2025 15:29:53 +0100 Subject: [PATCH 04/72] Resolve TODO --- src/CrossChainReceiverFactory.sol | 55 ++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index 62064da1c..b32f9911e 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -51,6 +51,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte // TODO: should the `MultiCall` EIP712 type include an (optional) relayer field that binds the metatransaction to a specific relayer? bytes32 private constant _MULTICALL_TYPEHASH = 0xd0290069becb7f8c7bc360deb286fb78314d4fb3e65d17004248ee046bd770a9; + bytes32 private constant _CALL_TYPEHASH = 0xa8b3616b5b84550a806f58ebe7d19199754b9632d31e5e6d07e7faf21fe1cacc; IERC20 private constant _NATIVE = IERC20(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); @@ -122,6 +123,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte "MultiCall(Call[] calls,uint256 contextdepth,uint256 nonce,uint256 deadline)Call(address target,uint8 revertPolicy,uint256 value,bytes data)" ) ); + require(_CALL_TYPEHASH = keccak256("Call(address target,uint8 revertPolicy,uint256 value,bytes data)")); // do some behavioral checks on `_WNATIVE` { @@ -493,12 +495,63 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte } } + // This function intentionally ignores any dirty bits that might be present in `calls`, assuming + // that: + // 1. The signer of the object wouldn't sign an invalid EIP712 serialization of the object + // containing dirty bits + // 2. The object will be used later in this context in a way that *does* check for dirty bits function _hashMultiCall(IMultiCall.Call[] calldata calls, uint256 contextdepth, uint256 nonce, uint256 deadline) private pure returns (bytes32 structHash) { - // TODO: + assembly ("memory-safe") { + let ptr := mload(0x40) + let lenBytes := shl(0x05, calls.length) + let arr := add(0xa0, ptr) + for { + let i + mstore(ptr, _CALL_TYPEHASH) + } xor(i, lenBytes) { i := add(0x20, i) } { + let dst := add(i, arr) + let src := add(i, calls.offset) + + // indirect `src` because it points to a dynamic type + src := add(calls.offset, calldataload(src)) + + // indirect `src.data` because it also points to a dynamic type + let data := add(0x60, src) + data := add(calldataload(data), src) + + // decode the length of `src.data` and hash it + { + let dataLength := calldataload(data) + data := add(0x20, data) + calldatacopy(dst, data, dataLength) + data := keccak256(dst, dataLength) + } + + // EIP712-encode the fixed-length fields of the `Call` object + calldatacopy(add(0x20, ptr), src, 0x60) + mstore(add(0x80, ptr), data) + + // hash the `Call` object into the `Call[]` array at `arr[i]` + mstore(dst, keccak256(ptr, 0xa0)) + } + + // hash the `Call[]` array + arr := keccak256(arr, lenBytes) + + // EIP712-encode the `MultiCall` + mstore(ptr, _MULTICALL_TYPEHASH) + mstore(add(0x20, ptr), arr) + mstore(add(0x40, ptr), contextdepth) + mstore(add(0x60, ptr), nonce) + mstore(add(0x80, ptr), deadline) + + // final hashing + structHash := keccak256(ptr, 0xa0) + } } function metaTx( From b28fb7e207c82db7e42b4611eec26904bd63cae3 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Wed, 26 Nov 2025 15:55:19 +0100 Subject: [PATCH 05/72] Resolve TODO --- src/CrossChainReceiverFactory.sol | 76 +++++++++++++++++++++++++++++-- 1 file changed, 72 insertions(+), 4 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index b32f9911e..73b07b0ed 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -418,7 +418,9 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte returndatacopy(add(0x40, ptr), 0x00, returndatasize()) - if iszero(success) { revert(add(0x40, ptr), returndatasize()) } + if iszero(success) { revert(add(0x40, mload(0x40)), returndatasize()) } + + // TODO: check for non-value-sending calls to empty addresses mstore(add(0x20, ptr), returndatasize()) mstore(ptr, 0x20) @@ -426,12 +428,78 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte } } - function call(address payable target, IERC20 token, uint256 patchOffset, bytes calldata data) + function call(address payable target, IERC20 token, uint256 ppm, uint256 patchOffset, bytes calldata data) external + payable override onlyOwnerOrSelf + returns (bytes memory) { - // TODO: + assembly ("memory-safe") { + // TODO: allow sending empty data with `offset == 0` if `token == _NATIVE` + if iszero(lt(add(0x1f, patchOffset), data.length)) { + mstore(0x00, 0x4e487b71) // selector for `Panic(uint256)` + mstore(0x20, 0x32) // code for array out-of-bounds + revert(0x1c, 0x24) + } + + let patchBytes + let value + for {} true {} { + if shl(0x60, xor(_NATIVE, token)) { + mstore(0x00, 0x70a08231) // `IERC20.balanceOf.selector` + mstore(0x20, address()) + if iszero(staticcall(gas(), token, 0x1c, 0x24, 0x00, 0x20)) { + let ptr_ := mload(0x40) + returndatacopy(ptr_, 0x00, returndatasize()) + revert(ptr_, returndatasize()) + } + if gt(0x20, returndatasize()) { revert(codesize(), 0x00) } + + let thisBalance := mload(0x00) + patchBytes := mul(ppm, thisBalance) + if xor(div(patchBytes, ppm), thisBalance) { + mstore(0x00, 0x4e487b71) // selector for `Panic(uint256)` + mstore(0x20, 0x11) // code for arithmetic overflow + revert(0x1c, 0x24) + } + + patchBytes := div(patchBytes, 1_000_000) + value := callvalue() + + break + } + + patchBytes := mul(ppm, selfbalance()) + if xor(div(patchBytes, ppm), selfbalance()) { + mstore(0x00, 0x4e487b71) // selector for `Panic(uint256)` + mstore(0x20, 0x11) // code for arithmetic overflow + revert(0x1c, 0x24) + } + + patchBytes := div(patchBytes, 1000000) + value := patchBytes + + break + } + + let ptr := mload(0x40) + calldatacopy(ptr, data.offset, data.length) + mstore(add(patchOffset, ptr), patchBytes) + + let failure := iszero(call(gas(), target, value, ptr, data.length, codesize(), 0x00)) + if iszero(or(failure, or(returndatasize(), value))) { + if iszero(extcodesize(target)) { revert(0x00, 0x00) } + } + + returndatacopy(add(0x40, ptr), 0x00, returndatasize()) + + if failure { revert(add(0x40, mload(0x40)), returndatasize()) } + + mstore(add(0x20, ptr), returndatasize()) + mstore(ptr, 0x20) + return(ptr, add(0x40, returndatasize())) + } } function _useUnorderedNonce(uint256 nonce) private { @@ -542,7 +610,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte // hash the `Call[]` array arr := keccak256(arr, lenBytes) - // EIP712-encode the `MultiCall` + // EIP712-encode the `MultiCall` object mstore(ptr, _MULTICALL_TYPEHASH) mstore(add(0x20, ptr), arr) mstore(add(0x40, ptr), contextdepth) From b2237a5d73869c091939e37a33a38319049a78ce Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Wed, 26 Nov 2025 16:03:00 +0100 Subject: [PATCH 06/72] Strict ABI encoding of return values --- src/CrossChainReceiverFactory.sol | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index 73b07b0ed..842a5fd11 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -416,6 +416,8 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte calldatacopy(ptr, data.offset, data.length) let success := call(gas(), target, value, ptr, data.length, codesize(), returndatasize()) + let paddedLength := and(not(0x1f), add(0x1f, returndatasize())) + mstore(add(add(0x20, ptr), paddedLength), 0x00) returndatacopy(add(0x40, ptr), 0x00, returndatasize()) if iszero(success) { revert(add(0x40, mload(0x40)), returndatasize()) } @@ -424,7 +426,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte mstore(add(0x20, ptr), returndatasize()) mstore(ptr, 0x20) - return(ptr, add(0x40, returndatasize())) + return(ptr, add(0x40, paddedLength)) } } @@ -492,13 +494,15 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte if iszero(extcodesize(target)) { revert(0x00, 0x00) } } + let paddedLength := and(not(0x1f), add(0x1f, returndatasize())) + mstore(add(add(0x20, ptr), paddedLength), 0x00) returndatacopy(add(0x40, ptr), 0x00, returndatasize()) if failure { revert(add(0x40, mload(0x40)), returndatasize()) } mstore(add(0x20, ptr), returndatasize()) mstore(ptr, 0x20) - return(ptr, add(0x40, returndatasize())) + return(ptr, add(0x40, paddedLength)) } } From 8af3eaac7c1d4471a93dc78f285f22fdc45e7232 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Wed, 26 Nov 2025 16:19:58 +0100 Subject: [PATCH 07/72] Resolve TODO --- src/CrossChainReceiverFactory.sol | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index 842a5fd11..0a48fcb01 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -414,15 +414,18 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte let ptr := mload(0x40) calldatacopy(ptr, data.offset, data.length) - let success := call(gas(), target, value, ptr, data.length, codesize(), returndatasize()) + let failure := iszero(call(gas(), target, value, ptr, data.length, codesize(), returndatasize())) + + // prohibit sending data or zero native asset to EOAs + if iszero(or(failure, or(returndatasize(), value))) { + if iszero(extcodesize(target)) { revert(0x00, 0x00) } + } let paddedLength := and(not(0x1f), add(0x1f, returndatasize())) mstore(add(add(0x20, ptr), paddedLength), 0x00) returndatacopy(add(0x40, ptr), 0x00, returndatasize()) - if iszero(success) { revert(add(0x40, mload(0x40)), returndatasize()) } - - // TODO: check for non-value-sending calls to empty addresses + if failure { revert(add(0x40, mload(0x40)), returndatasize()) } mstore(add(0x20, ptr), returndatasize()) mstore(ptr, 0x20) @@ -490,6 +493,8 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte mstore(add(patchOffset, ptr), patchBytes) let failure := iszero(call(gas(), target, value, ptr, data.length, codesize(), 0x00)) + + // prohibit sending data or zero native asset to EOAs if iszero(or(failure, or(returndatasize(), value))) { if iszero(extcodesize(target)) { revert(0x00, 0x00) } } From 83fa34aa026bdacc1a417f7f2d020599cf4a0f39 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Wed, 26 Nov 2025 16:22:23 +0100 Subject: [PATCH 08/72] Bug! `XOR` instead of `AND` when checking for nonce replay --- src/CrossChainReceiverFactory.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index 0a48fcb01..dac1367df 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -524,7 +524,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte revert(ptr, returndatasize()) } let canceledNonces := mload(returndatasize()) - if xor(canceledNonces, bitPos) { + if and(canceledNonces, bitPos) { mstore(returndatasize(), 0x756688fe) // `InvalidNonce.selector` revert(0x3c, 0x04) } From b65886d0ed9182b7584ada41dfa1df3911693c9a Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Wed, 26 Nov 2025 16:23:15 +0100 Subject: [PATCH 09/72] Bug! Flipped `SHL` --- src/CrossChainReceiverFactory.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index dac1367df..f2d814b58 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -515,7 +515,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte assembly ("memory-safe") { let ptr := mload(0x40) let wordPos := shr(0x08, nonce) - let bitPos := shl(0x01, and(0xff, nonce)) + let bitPos := shl(and(0xff, nonce), 0x01) mstore(0x00, 0x4fe02b44) // `ISignatureTransfer.nonceBitmap.selector` mstore(0x20, address()) mstore(0x40, wordPos) From bc38687fe82f120ee6cf2301fc4fd27f7ca3e520 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Wed, 26 Nov 2025 16:42:33 +0100 Subject: [PATCH 10/72] Constrain the `nonce` field to make certain kinds of confusion less likely --- src/CrossChainReceiverFactory.sol | 67 +++++++++++++++++-------------- 1 file changed, 37 insertions(+), 30 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index f2d814b58..a9c846ef3 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -49,7 +49,9 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte bytes32 private constant _NAMEHASH = 0x819c7f86c24229cd5fed5a41696eb0cd8b3f84cc632df73cfd985e8b100980e8; bytes32 private constant _DOMAIN_TYPEHASH = 0x8cad95687ba82c2ce50e74f7b754645e5117c3a5bec8151c0726d5857980a866; - // TODO: should the `MultiCall` EIP712 type include an (optional) relayer field that binds the metatransaction to a specific relayer? + // TODO: should the `MultiCall` EIP712 type include an (optional) relayer field that binds the + // metatransaction to a specific relayer? Perhaps this ought to be encoded in the `deadline` + // field similarly to how the `nonce` field encodes the current owner. bytes32 private constant _MULTICALL_TYPEHASH = 0xd0290069becb7f8c7bc360deb286fb78314d4fb3e65d17004248ee046bd770a9; bytes32 private constant _CALL_TYPEHASH = 0xa8b3616b5b84550a806f58ebe7d19199754b9632d31e5e6d07e7faf21fe1cacc; @@ -256,8 +258,17 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte } if (validOwner) { - (bytes32 leafHash, bytes32[] calldata proof) = _formatMerkleProof(hash, signature); - return _verifyDeploymentRootHash(_getMerkleRoot(proof, leafHash), originalOwner).ternary( + bytes32[] calldata proof; + assembly ("memory-safe") { + // This assembly block simply ABIDecodes `proof` as the second element of the + // encoded anonymous struct `(owner, proof)`. It omits range and overflow + // checking. + // (, proof) = abi.decode(signature, (address, bytes32[])); + proof.offset := add(signature.offset, calldataload(add(0x20, signature.offset))) + proof.length := calldataload(proof.offset) + proof.offset := add(0x20, proof.offset) + } + return _verifyDeploymentRootHash(_getMerkleRoot(proof, _hashLeaf(signingHash)), originalOwner).ternary( IERC1271.isValidSignature.selector, bytes4(0xffffffff) ); } @@ -269,11 +280,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte ); } - function _formatMerkleProof(bytes32 hash, bytes calldata signature) - private - view - returns (bytes32 leafHash, bytes32[] calldata proof) - { + function _hashLeaf(bytes32 signingHash) private view returns (bytes32 leafHash) { assembly ("memory-safe") { // This assembly block is equivalent to: // hash = keccak256(abi.encode(hash, block.chainid)); @@ -287,16 +294,6 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte mstore(0x20, chainid()) leafHash := keccak256(0x00, 0x40) } - - bytes32[] calldata proof; - assembly ("memory-safe") { - // This assembly block simply ABIDecodes `proof` as the second element of the encoded - // anonymous struct `(owner, proof)`. It omits range and overflow checking. - // (, proof) = abi.decode(signature, (address, bytes32[])); - proof.offset := add(signature.offset, calldataload(add(0x20, signature.offset))) - proof.length := calldataload(proof.offset) - proof.offset := add(0x20, proof.offset) - } } /// @inheritdoc IERC5267 @@ -646,28 +643,38 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte revert(0x1c, 0x24) } } - _useUnorderedNonce(nonce); bytes32 signingHash = _hashEip712(_hashMultiCall(calls, contextdepth, nonce, deadline)); - address owner_; - bool validOwner; - assembly ("memory-safe") { - owner_ := calldataload(signature.offset) - validOwner := iszero(shr(0xa0, owner_)) - } - if (validOwner) { - (bytes32 leafHash, bytes32[] calldata proof) = _formatMerkleProof(signingHash, signature); - if (!_verifyDeploymentRootHash(_getMerkleRoot(proof, leafHash), owner_)) { + // The upper 160 bits of the nonce encode the owner + address owner_ = address(uint160(nonce >> 96)); + if (owner_ != address(0)) { + bytes32[] calldata proof; + assembly ("memory-safe") { + // This assembly block simply ABIDecodes `proof` from `signature`. It omits range + // and overflow checking. + // proof = abi.decode(signature, (bytes32[])); + proof.offset := add(signature.offset, calldataload(signature.offset)) + proof.length := calldataload(proof.offset) + proof.offset := add(0x20, proof.offset) + } + if (!_verifyDeploymentRootHash(_getMerkleRoot(proof, _hashLeaf(signingHash)), owner_)) { assembly ("memory-safe") { mstore(0x00, 0x815e1d64) // `InvalidSigner.selector` revert(0x1c, 0x04) } } } else { - owner_ = super.owner(); - _verifySimpleSignature(signingHash, signature, owner_); + _verifySimpleSignature(signingHash, signature, owner_ = super.owner()); + + // `nonce`'s upper 160 bits must be the current owner. This prevents "Nick's Method" + // shenanigans as well as avoiding potential confusion when ownership is + // transferred. Obviously if ownership is transferred *back* then confusion may occur, + // but the `deadline` field should limit the blast radius of failures like that. + nonce |= uint256(uint160(owner_)) << 96; } + _useUnorderedNonce(nonce); + return IMultiCall(MULTICALL_ADDRESS).multicall(calls, contextdepth); } From e68056282581232106e96ddc05c6b81d975e837e Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Wed, 26 Nov 2025 16:46:18 +0100 Subject: [PATCH 11/72] Make all `onlyOwner` functions `onlyOwnerOrSelf` --- src/CrossChainReceiverFactory.sol | 18 ++++++++++-------- src/utils/TwoStepOwnable.sol | 2 +- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index a9c846ef3..63d2d2ac9 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -175,12 +175,11 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte _; } - modifier onlyOwnerOrSelf() { + function _requireOwner() internal view override { address msgSender = _msgSender(); if (msgSender != address(this) && msgSender != owner()) { _permissionDenied(); } - _; } /// @inheritdoc IERC165 @@ -404,7 +403,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte function call(address payable target, uint256 value, bytes calldata data) external override - onlyOwnerOrSelf + onlyOwner returns (bytes memory) { assembly ("memory-safe") { @@ -434,7 +433,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte external payable override - onlyOwnerOrSelf + onlyOwner returns (bytes memory) { assembly ("memory-safe") { @@ -643,11 +642,13 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte revert(0x1c, 0x24) } } - bytes32 signingHash = _hashEip712(_hashMultiCall(calls, contextdepth, nonce, deadline)); // The upper 160 bits of the nonce encode the owner address owner_ = address(uint160(nonce >> 96)); + if (owner_ != address(0)) { + bytes32 signingHash = _hashEip712(_hashMultiCall(calls, contextdepth, nonce, deadline)); + bytes32[] calldata proof; assembly ("memory-safe") { // This assembly block simply ABIDecodes `proof` from `signature`. It omits range @@ -664,13 +665,14 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte } } } else { - _verifySimpleSignature(signingHash, signature, owner_ = super.owner()); - - // `nonce`'s upper 160 bits must be the current owner. This prevents "Nick's Method" + // `nonce`'s upper 160 bits must be the *current* owner. This prevents "Nick's Method" // shenanigans as well as avoiding potential confusion when ownership is // transferred. Obviously if ownership is transferred *back* then confusion may occur, // but the `deadline` field should limit the blast radius of failures like that. nonce |= uint256(uint160(owner_)) << 96; + bytes32 signingHash = _hashEip712(_hashMultiCall(calls, contextdepth, nonce, deadline)); + + _verifySimpleSignature(signingHash, signature, owner_ = super.owner()); } _useUnorderedNonce(nonce); diff --git a/src/utils/TwoStepOwnable.sol b/src/utils/TwoStepOwnable.sol index 0b4441d69..bcff327e5 100644 --- a/src/utils/TwoStepOwnable.sol +++ b/src/utils/TwoStepOwnable.sol @@ -106,7 +106,7 @@ abstract contract OwnableImpl is OwnableStorageBase, OwnableBase { _set(_ownerSlot(), newOwner); } - function _requireOwner() internal view override { + function _requireOwner() internal view virtual override { if (owner() != _msgSender()) { _permissionDenied(); } From b02888f70fee58eafc46cfccff28e64b37b3ca17 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Wed, 26 Nov 2025 17:06:43 +0100 Subject: [PATCH 12/72] Compilation errors --- src/CrossChainReceiverFactory.sol | 41 +++++++++++-------- src/interfaces/ICrossChainReceiverFactory.sol | 16 ++++++++ 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index 63d2d2ac9..87c0446cd 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -10,7 +10,7 @@ import {IOwnable} from "./interfaces/IOwnable.sol"; import {ISignatureTransfer} from "@permit2/interfaces/ISignatureTransfer.sol"; import {ICrossChainReceiverFactory} from "./interfaces/ICrossChainReceiverFactory.sol"; -import {AbstractOwnable, TwoStepOwnable} from "./utils/TwoStepOwnable.sol"; +import {AbstractOwnable, OwnableImpl, TwoStepOwnable} from "./utils/TwoStepOwnable.sol"; import {IMultiCall, MultiCallContext, MULTICALL_ADDRESS} from "./multicall/MultiCallContext.sol"; import {FastLogic} from "./utils/FastLogic.sol"; @@ -36,7 +36,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte bytes32 private immutable _proxyInitCode0 = bytes32(bytes20(0x60253d8160093d39F33d3d3D3D363D3D37363d6C)) | bytes32(uint256(uint160(address(this))) >> 8); bytes32 private immutable _proxyInitCode1 = - bytes32(bytes1(uint8(uint160(address(this))))) | bytes32(0x5af43d3d93803e602357fd5bf3 << 144); + bytes32(bytes1(uint8(uint160(address(this))))) | bytes32(uint256(0x5af43d3d93803e602357fd5bf3 << 144)); bytes32 private immutable _proxyInitHash = keccak256( bytes.concat( hex"60253d8160093d39f33d3d3d3d363d3d37363d6c", @@ -55,7 +55,8 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte bytes32 private constant _MULTICALL_TYPEHASH = 0xd0290069becb7f8c7bc360deb286fb78314d4fb3e65d17004248ee046bd770a9; bytes32 private constant _CALL_TYPEHASH = 0xa8b3616b5b84550a806f58ebe7d19199754b9632d31e5e6d07e7faf21fe1cacc; - IERC20 private constant _NATIVE = IERC20(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); + address private constant _NATIVE_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + IERC20 private constant _NATIVE = IERC20(_NATIVE_ADDRESS); address private constant _TOEHOLD = 0x4e59b44847b379578588920cA78FbF26c0B4956C; address private constant _WNATIVE_SETTER = 0x000000000000F01B1D1c8EEF6c6cF71a0b658Fbc; @@ -99,7 +100,8 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte IWrappedNative private immutable _WNATIVE = IWrappedNative(payable(address(uint160(uint256(bytes32(_WNATIVE_STORAGE.code)))))); - ISignatureTransfer private constant _PERMIT2 = ISignatureTransfer(0x000000000022D473030F116dDEE9F6B43aC78BA3); + address private constant _PERMIT2_ADDRESS = 0x000000000022D473030F116dDEE9F6B43aC78BA3; + ISignatureTransfer private constant _PERMIT2 = ISignatureTransfer(_PERMIT2_ADDRESS); error DeploymentFailed(); error ApproveFailed(); @@ -121,11 +123,12 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte require(_NAMEHASH == keccak256(bytes(name))); require(_DOMAIN_TYPEHASH == keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)")); require( - _MULTICALL_TYPEHASH = keccak256( - "MultiCall(Call[] calls,uint256 contextdepth,uint256 nonce,uint256 deadline)Call(address target,uint8 revertPolicy,uint256 value,bytes data)" - ) + _MULTICALL_TYPEHASH + == keccak256( + "MultiCall(Call[] calls,uint256 contextdepth,uint256 nonce,uint256 deadline)Call(address target,uint8 revertPolicy,uint256 value,bytes data)" + ) ); - require(_CALL_TYPEHASH = keccak256("Call(address target,uint8 revertPolicy,uint256 value,bytes data)")); + require(_CALL_TYPEHASH == keccak256("Call(address target,uint8 revertPolicy,uint256 value,bytes data)")); // do some behavioral checks on `_WNATIVE` { @@ -175,7 +178,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte _; } - function _requireOwner() internal view override { + function _requireOwner() internal view override(AbstractOwnable, OwnableImpl) { address msgSender = _msgSender(); if (msgSender != address(this) && msgSender != owner()) { _permissionDenied(); @@ -267,7 +270,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte proof.length := calldataload(proof.offset) proof.offset := add(0x20, proof.offset) } - return _verifyDeploymentRootHash(_getMerkleRoot(proof, _hashLeaf(signingHash)), originalOwner).ternary( + return _verifyDeploymentRootHash(_getMerkleRoot(proof, _hashLeaf(hash)), originalOwner).ternary( IERC1271.isValidSignature.selector, bytes4(0xffffffff) ); } @@ -289,7 +292,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte // elsewhere in the ecosystem. This also means that the sort order of the hash and the // chainid is backwards from what `_getMerkleRoot` produces, again protecting us against // extension attacks. - mstore(0x00, hash) + mstore(0x00, signingHash) mstore(0x20, chainid()) leafHash := keccak256(0x00, 0x40) } @@ -381,7 +384,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte let ptr := mload(0x40) mstore(returndatasize(), 0x095ea7b3) // selector for `approve(address,uint256)` - mstore(0x20, _PERMIT2) + mstore(0x20, _PERMIT2_ADDRESS) mstore(0x40, amount) if iszero(call(gas(), token, callvalue(), 0x1c, 0x44, returndatasize(), 0x20)) { @@ -429,6 +432,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte } } + /// @inheritdoc ICrossChainReceiverFactory function call(address payable target, IERC20 token, uint256 ppm, uint256 patchOffset, bytes calldata data) external payable @@ -447,7 +451,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte let patchBytes let value for {} true {} { - if shl(0x60, xor(_NATIVE, token)) { + if shl(0x60, xor(_NATIVE_ADDRESS, token)) { mstore(0x00, 0x70a08231) // `IERC20.balanceOf.selector` mstore(0x20, address()) if iszero(staticcall(gas(), token, 0x1c, 0x24, 0x00, 0x20)) { @@ -465,7 +469,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte revert(0x1c, 0x24) } - patchBytes := div(patchBytes, 1_000_000) + patchBytes := div(patchBytes, 1000000) value := callvalue() break @@ -515,7 +519,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte mstore(0x00, 0x4fe02b44) // `ISignatureTransfer.nonceBitmap.selector` mstore(0x20, address()) mstore(0x40, wordPos) - if iszero(staticcall(gas(), _PERMIT2, 0x1c, 0x44, 0x20, 0x20)) { + if iszero(staticcall(gas(), _PERMIT2_ADDRESS, 0x1c, 0x44, 0x20, 0x20)) { returndatacopy(ptr, 0x00, returndatasize()) revert(ptr, returndatasize()) } @@ -527,7 +531,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte mstore(0x00, 0x3ff9dcb1) // `ISignatureTransfer.invalidateUnorderedNonces.selector` mstore(returndatasize(), wordPos) mstore(0x40, bitPos) - if iszero(call(gas(), _PERMIT2, 0x00, 0x1c, 0x44, codesize(), 0x00)) { + if iszero(call(gas(), _PERMIT2_ADDRESS, 0x00, 0x1c, 0x44, codesize(), 0x00)) { returndatacopy(ptr, 0x00, returndatasize()) revert(ptr, returndatasize()) } @@ -551,7 +555,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte } } - function _hashEip712(bytes32 structHash) private pure returns (bytes32 signingHash) { + function _hashEip712(bytes32 structHash) private view returns (bytes32 signingHash) { assembly ("memory-safe") { let ptr := mload(0x40) mstore(0x00, _DOMAIN_TYPEHASH) @@ -627,6 +631,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte } } + /// @inheritdoc ICrossChainReceiverFactory function metaTx( IMultiCall.Call[] calldata calls, uint256 contextdepth, @@ -677,7 +682,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte _useUnorderedNonce(nonce); - return IMultiCall(MULTICALL_ADDRESS).multicall(calls, contextdepth); + return IMultiCall(payable(MULTICALL_ADDRESS)).multicall(calls, contextdepth); } /// @inheritdoc ICrossChainReceiverFactory diff --git a/src/interfaces/ICrossChainReceiverFactory.sol b/src/interfaces/ICrossChainReceiverFactory.sol index f8963cd6f..3b14dcde3 100644 --- a/src/interfaces/ICrossChainReceiverFactory.sol +++ b/src/interfaces/ICrossChainReceiverFactory.sol @@ -5,6 +5,7 @@ import {IERC20} from "@forge-std/interfaces/IERC20.sol"; import {IERC1271} from "./IERC1271.sol"; import {IERC5267} from "./IERC5267.sol"; import {IOwnable} from "./IOwnable.sol"; +import {IMultiCall} from "../multicall/MultiCallContext.sol"; interface ICrossChainReceiverFactory is IERC1271, IERC5267, IOwnable { function name() external view returns (string memory); @@ -23,6 +24,21 @@ interface ICrossChainReceiverFactory is IERC1271, IERC5267, IOwnable { /// Only available on proxies function call(address payable target, uint256 value, bytes calldata data) external returns (bytes memory); + /// Only available on proxies + function call(address payable target, IERC20 token, uint256 ppm, uint256 patchOffset, bytes calldata data) + external + payable + returns (bytes memory); + + /// Only available on proxies + function metaTx( + IMultiCall.Call[] calldata calls, + uint256 contextdepth, + uint256 nonce, + uint256 deadline, + bytes calldata signature + ) external returns (IMultiCall.Result[] memory); + /// Only available on proxies function cleanup(address payable beneficiary) external; From ce4f10885230dd120c9af3f022aa37391395cb34 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Wed, 26 Nov 2025 17:20:00 +0100 Subject: [PATCH 13/72] Bounds checking for signature length --- src/CrossChainReceiverFactory.sol | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index 87c0446cd..6ae8e2683 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -541,6 +541,12 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte function _verifySimpleSignature(bytes32 signingHash, bytes calldata rsv, address owner_) private view { assembly ("memory-safe") { + if gt(0x41, rsv.length) { + mstore(0x00, 0x4e487b71) // selector for `Panic(uint256)` + mstore(0x20, 0x32) // code for array out-of-bounds + revert(0x1c, 0x24) + } + let ptr := mload(0x40) mstore(0x00, signingHash) calldatacopy(0x20, rsv.offset, 0x41) From 1a5dde852df3e4eafbaa06c5b12e31d9d214762e Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Wed, 26 Nov 2025 18:01:04 +0100 Subject: [PATCH 14/72] As the comment says, prohibit sending **data** to EOAs --- src/CrossChainReceiverFactory.sol | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index 6ae8e2683..d195a8e23 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -413,10 +413,10 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte let ptr := mload(0x40) calldatacopy(ptr, data.offset, data.length) - let failure := iszero(call(gas(), target, value, ptr, data.length, codesize(), returndatasize())) + let success := iszero(call(gas(), target, value, ptr, data.length, codesize(), returndatasize())) // prohibit sending data or zero native asset to EOAs - if iszero(or(failure, or(returndatasize(), value))) { + if gt(lt(returndatasize(), success), mul(iszero(data.length), value)) { if iszero(extcodesize(target)) { revert(0x00, 0x00) } } @@ -424,7 +424,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte mstore(add(add(0x20, ptr), paddedLength), 0x00) returndatacopy(add(0x40, ptr), 0x00, returndatasize()) - if failure { revert(add(0x40, mload(0x40)), returndatasize()) } + if iszero(success) { revert(add(0x40, mload(0x40)), returndatasize()) } mstore(add(0x20, ptr), returndatasize()) mstore(ptr, 0x20) @@ -492,10 +492,10 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte calldatacopy(ptr, data.offset, data.length) mstore(add(patchOffset, ptr), patchBytes) - let failure := iszero(call(gas(), target, value, ptr, data.length, codesize(), 0x00)) + let success := call(gas(), target, value, ptr, data.length, codesize(), 0x00) // prohibit sending data or zero native asset to EOAs - if iszero(or(failure, or(returndatasize(), value))) { + if gt(lt(returndatasize(), success), mul(iszero(data.length), value)) { if iszero(extcodesize(target)) { revert(0x00, 0x00) } } @@ -503,7 +503,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte mstore(add(add(0x20, ptr), paddedLength), 0x00) returndatacopy(add(0x40, ptr), 0x00, returndatasize()) - if failure { revert(add(0x40, mload(0x40)), returndatasize()) } + if iszero(success) { revert(add(0x40, mload(0x40)), returndatasize()) } mstore(add(0x20, ptr), returndatasize()) mstore(ptr, 0x20) From 965738ebe0b8497b0b508423980114946d887610 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Wed, 26 Nov 2025 21:59:32 +0100 Subject: [PATCH 15/72] Golf --- src/CrossChainReceiverFactory.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index d195a8e23..fe46bdd46 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -416,7 +416,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte let success := iszero(call(gas(), target, value, ptr, data.length, codesize(), returndatasize())) // prohibit sending data or zero native asset to EOAs - if gt(lt(returndatasize(), success), mul(iszero(data.length), value)) { + if lt(or(returndatasize(), mul(iszero(data.length), value)), success) { if iszero(extcodesize(target)) { revert(0x00, 0x00) } } @@ -495,7 +495,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte let success := call(gas(), target, value, ptr, data.length, codesize(), 0x00) // prohibit sending data or zero native asset to EOAs - if gt(lt(returndatasize(), success), mul(iszero(data.length), value)) { + if lt(or(returndatasize(), mul(iszero(data.length), value)), success) { if iszero(extcodesize(target)) { revert(0x00, 0x00) } } From 1c59e96362244637a56ef6e2d0c52afdd45858d7 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Wed, 26 Nov 2025 22:02:46 +0100 Subject: [PATCH 16/72] Resolve warning --- src/CrossChainReceiverFactory.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index fe46bdd46..3df872d18 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -364,8 +364,8 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte } /// @inheritdoc ICrossChainReceiverFactory - function setOwner(address owner) external override onlyFactory { - _setOwner(owner); + function setOwner(address owner_) external override onlyFactory { + _setOwner(owner_); } /// @inheritdoc ICrossChainReceiverFactory From f57484205c93979c2c1aba7f74afce2e8fce9fb8 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Wed, 26 Nov 2025 22:30:27 +0100 Subject: [PATCH 17/72] Golf; add TODO --- src/CrossChainReceiverFactory.sol | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index 3df872d18..4146c0b28 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -52,6 +52,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte // TODO: should the `MultiCall` EIP712 type include an (optional) relayer field that binds the // metatransaction to a specific relayer? Perhaps this ought to be encoded in the `deadline` // field similarly to how the `nonce` field encodes the current owner. + // TODO: add a `value` field bytes32 private constant _MULTICALL_TYPEHASH = 0xd0290069becb7f8c7bc360deb286fb78314d4fb3e65d17004248ee046bd770a9; bytes32 private constant _CALL_TYPEHASH = 0xa8b3616b5b84550a806f58ebe7d19199754b9632d31e5e6d07e7faf21fe1cacc; @@ -676,8 +677,8 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte } } } else { - // `nonce`'s upper 160 bits must be the *current* owner. This prevents "Nick's Method" - // shenanigans as well as avoiding potential confusion when ownership is + // `nonce`'s upper 160 bits will encode the *current* owner. This prevents "Nick's + // Method" shenanigans as well as avoiding potential confusion when ownership is // transferred. Obviously if ownership is transferred *back* then confusion may occur, // but the `deadline` field should limit the blast radius of failures like that. nonce |= uint256(uint160(owner_)) << 96; @@ -688,7 +689,20 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte _useUnorderedNonce(nonce); - return IMultiCall(payable(MULTICALL_ADDRESS)).multicall(calls, contextdepth); + assembly ("memory-safe") { + let ptr := mload(0x40) + calldatacopy(0x1c, 0x00, calldatasize()) + mstore(0x00, 0x669a7d5e) // `IMultiCall.multicall.selector` + + let success := call(gas(), MULTICALL_ADDRESS, 0x00 /* TODO: */, 0x1c, calldatasize(), codesize(), 0x00) + + returndatacopy(ptr, 0x00, returndatasize()) + + if iszero(success) { + revert(ptr, returndatasize()) + } + return(ptr, returndata()) + } } /// @inheritdoc ICrossChainReceiverFactory From ac20e917feb344d12e487274e2b9f744d9f6d6a7 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Wed, 26 Nov 2025 22:31:27 +0100 Subject: [PATCH 18/72] Remove resolved TODO --- src/CrossChainReceiverFactory.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index 4146c0b28..cc6ab365e 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -646,7 +646,6 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte uint256 deadline, bytes calldata signature ) external override onlyProxy returns (IMultiCall.Result[] memory) { - // TODO: make this more checks-effects-interactions if (block.timestamp > deadline) { assembly ("memory-safe") { mstore(returndatasize(), 0xcd21db4f) // `SignatureExpired.selector` From a6a5b3e8a6d842082eb2490dcf693cbaa9d51954 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Thu, 27 Nov 2025 09:25:14 +0100 Subject: [PATCH 19/72] Resolve TODO --- src/CrossChainReceiverFactory.sol | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index cc6ab365e..58c41cf86 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -49,9 +49,6 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte bytes32 private constant _NAMEHASH = 0x819c7f86c24229cd5fed5a41696eb0cd8b3f84cc632df73cfd985e8b100980e8; bytes32 private constant _DOMAIN_TYPEHASH = 0x8cad95687ba82c2ce50e74f7b754645e5117c3a5bec8151c0726d5857980a866; - // TODO: should the `MultiCall` EIP712 type include an (optional) relayer field that binds the - // metatransaction to a specific relayer? Perhaps this ought to be encoded in the `deadline` - // field similarly to how the `nonce` field encodes the current owner. // TODO: add a `value` field bytes32 private constant _MULTICALL_TYPEHASH = 0xd0290069becb7f8c7bc360deb286fb78314d4fb3e65d17004248ee046bd770a9; bytes32 private constant _CALL_TYPEHASH = 0xa8b3616b5b84550a806f58ebe7d19199754b9632d31e5e6d07e7faf21fe1cacc; @@ -653,6 +650,15 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte revert(0x1c, 0x24) } } + { + address relayer = address(uint160(deadline >> 96)); + if (relayer != address(0)) { + if (relayer != _msgSender()) { + _permissionDenied(); + } + deadline &= 0xffffffffffffffffffffffff; + } + } // The upper 160 bits of the nonce encode the owner address owner_ = address(uint160(nonce >> 96)); From 049bbad6dcde826ec69399bb9a56fc19a5e11a55 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Thu, 27 Nov 2025 09:25:52 +0100 Subject: [PATCH 20/72] Don't re-forward ERC2771 metadata --- src/CrossChainReceiverFactory.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index 58c41cf86..a8d7dd2a7 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -694,12 +694,13 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte _useUnorderedNonce(nonce); + bytes calldata msgData = _msgData(); assembly ("memory-safe") { let ptr := mload(0x40) - calldatacopy(0x1c, 0x00, calldatasize()) + calldatacopy(0x1c, msgData.offset, msgData.length) mstore(0x00, 0x669a7d5e) // `IMultiCall.multicall.selector` - let success := call(gas(), MULTICALL_ADDRESS, 0x00 /* TODO: */, 0x1c, calldatasize(), codesize(), 0x00) + let success := call(gas(), MULTICALL_ADDRESS, 0x00 /* TODO: */, 0x1c, msgData.length, codesize(), 0x00) returndatacopy(ptr, 0x00, returndatasize()) From 541e2dec738594e70370267c4937a4afba4dafd4 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Thu, 27 Nov 2025 10:45:46 +0100 Subject: [PATCH 21/72] Bug! Inverted `success` as a typo from previous changes --- src/CrossChainReceiverFactory.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index a8d7dd2a7..d851f312f 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -411,7 +411,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte let ptr := mload(0x40) calldatacopy(ptr, data.offset, data.length) - let success := iszero(call(gas(), target, value, ptr, data.length, codesize(), returndatasize())) + let success := call(gas(), target, value, ptr, data.length, codesize(), returndatasize()) // prohibit sending data or zero native asset to EOAs if lt(or(returndatasize(), mul(iszero(data.length), value)), success) { From fdedb808d2f380bc76f047ae75ff0958a35dbf21 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Thu, 27 Nov 2025 10:47:13 +0100 Subject: [PATCH 22/72] Bug! Reversed two code blocks makes the deadline useless --- src/CrossChainReceiverFactory.sol | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index d851f312f..c790b4e86 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -643,13 +643,6 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte uint256 deadline, bytes calldata signature ) external override onlyProxy returns (IMultiCall.Result[] memory) { - if (block.timestamp > deadline) { - assembly ("memory-safe") { - mstore(returndatasize(), 0xcd21db4f) // `SignatureExpired.selector` - mstore(0x20, deadline) - revert(0x1c, 0x24) - } - } { address relayer = address(uint160(deadline >> 96)); if (relayer != address(0)) { @@ -659,6 +652,13 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte deadline &= 0xffffffffffffffffffffffff; } } + if (block.timestamp > deadline) { + assembly ("memory-safe") { + mstore(returndatasize(), 0xcd21db4f) // `SignatureExpired.selector` + mstore(0x20, deadline) + revert(0x1c, 0x24) + } + } // The upper 160 bits of the nonce encode the owner address owner_ = address(uint160(nonce >> 96)); From 080f38797cead7009af2a273a5c6c2fb263a1204 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Thu, 27 Nov 2025 10:47:42 +0100 Subject: [PATCH 23/72] Defensively make `_requireOwner()` explicitly `onlyProxy` --- src/CrossChainReceiverFactory.sol | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index c790b4e86..75042e8e8 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -176,9 +176,9 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte _; } - function _requireOwner() internal view override(AbstractOwnable, OwnableImpl) { + function _requireOwner() internal view onlyProxy override(AbstractOwnable, OwnableImpl) { address msgSender = _msgSender(); - if (msgSender != address(this) && msgSender != owner()) { + if (msgSender != address(this) && msgSender != super.owner()) { _permissionDenied(); } } @@ -195,8 +195,9 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte } // This function is overridden so that it is explicit that it is only meaningful on the - // proxy. This also makes any function that is `onlyOwner` implicitly `onlyProxy`, including - // `renounceOwnership` and `transferOwnership`. + // proxy. While this alone would ordinarily be sufficient to make any function that is + // `onlyOwner` implicitly `onlyProxy`, including `renounceOwnership` and `transferOwnership`, we + // have also explicitly made `_requireOwner()` `onlyProxy`. function owner() public view override(IOwnable, AbstractOwnable) onlyProxy returns (address) { return super.owner(); } From 0340eeb64d97f9a213eedc38c144c060e368ad1d Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Thu, 27 Nov 2025 10:50:12 +0100 Subject: [PATCH 24/72] Typo --- src/CrossChainReceiverFactory.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index 75042e8e8..d6b658b5f 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -708,7 +708,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte if iszero(success) { revert(ptr, returndatasize()) } - return(ptr, returndata()) + return(ptr, returndatasize()) } } From cb16377991bd8124163d7c1e78f3ff84393ecfb7 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Thu, 27 Nov 2025 12:03:30 +0100 Subject: [PATCH 25/72] Bug! Bad order of resetting `owner_` for non-Merkle metatx flows --- src/CrossChainReceiverFactory.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index d6b658b5f..2d8b29fa4 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -687,10 +687,11 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte // Method" shenanigans as well as avoiding potential confusion when ownership is // transferred. Obviously if ownership is transferred *back* then confusion may occur, // but the `deadline` field should limit the blast radius of failures like that. + owner_ = super.owner(); nonce |= uint256(uint160(owner_)) << 96; - bytes32 signingHash = _hashEip712(_hashMultiCall(calls, contextdepth, nonce, deadline)); - _verifySimpleSignature(signingHash, signature, owner_ = super.owner()); + bytes32 signingHash = _hashEip712(_hashMultiCall(calls, contextdepth, nonce, deadline)); + _verifySimpleSignature(signingHash, signature, owner_); } _useUnorderedNonce(nonce); From 1ef3e6a1b2b1ad88936c4f1cba28f85e8127a96f Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Thu, 27 Nov 2025 12:04:14 +0100 Subject: [PATCH 26/72] `forge fmt` --- src/CrossChainReceiverFactory.sol | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index 2d8b29fa4..b76f7bc5d 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -176,7 +176,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte _; } - function _requireOwner() internal view onlyProxy override(AbstractOwnable, OwnableImpl) { + function _requireOwner() internal view override(AbstractOwnable, OwnableImpl) onlyProxy { address msgSender = _msgSender(); if (msgSender != address(this) && msgSender != super.owner()) { _permissionDenied(); @@ -702,13 +702,11 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte calldatacopy(0x1c, msgData.offset, msgData.length) mstore(0x00, 0x669a7d5e) // `IMultiCall.multicall.selector` - let success := call(gas(), MULTICALL_ADDRESS, 0x00 /* TODO: */, 0x1c, msgData.length, codesize(), 0x00) + let success := call(gas(), MULTICALL_ADDRESS, 0x00, /* TODO: */ 0x1c, msgData.length, codesize(), 0x00) returndatacopy(ptr, 0x00, returndatasize()) - if iszero(success) { - revert(ptr, returndatasize()) - } + if iszero(success) { revert(ptr, returndatasize()) } return(ptr, returndatasize()) } } From 059fe4fbd4eb0181f98bf3850e36e37b90925942 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Thu, 27 Nov 2025 12:27:29 +0100 Subject: [PATCH 27/72] Bug! Use one deadline for `block.timestamp` checking and another for struct hashing --- src/CrossChainReceiverFactory.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index b76f7bc5d..f3ca02f4e 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -644,6 +644,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte uint256 deadline, bytes calldata signature ) external override onlyProxy returns (IMultiCall.Result[] memory) { + uint256 deadlineForHashing = deadline; { address relayer = address(uint160(deadline >> 96)); if (relayer != address(0)) { @@ -665,7 +666,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte address owner_ = address(uint160(nonce >> 96)); if (owner_ != address(0)) { - bytes32 signingHash = _hashEip712(_hashMultiCall(calls, contextdepth, nonce, deadline)); + bytes32 signingHash = _hashEip712(_hashMultiCall(calls, contextdepth, nonce, deadlineForHashing)); bytes32[] calldata proof; assembly ("memory-safe") { @@ -690,7 +691,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte owner_ = super.owner(); nonce |= uint256(uint160(owner_)) << 96; - bytes32 signingHash = _hashEip712(_hashMultiCall(calls, contextdepth, nonce, deadline)); + bytes32 signingHash = _hashEip712(_hashMultiCall(calls, contextdepth, nonce, deadlineForHashing)); _verifySimpleSignature(signingHash, signature, owner_); } From 165c06e83ac00bb2af8bc2af8f7f92f25ed0eece Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Thu, 27 Nov 2025 15:31:52 +0100 Subject: [PATCH 28/72] Improve comments --- src/CrossChainReceiverFactory.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index f3ca02f4e..05db5e913 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -414,7 +414,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte calldatacopy(ptr, data.offset, data.length) let success := call(gas(), target, value, ptr, data.length, codesize(), returndatasize()) - // prohibit sending data or zero native asset to EOAs + // prohibit sending data to EOAs; prohibit sending zero value to EOAs if lt(or(returndatasize(), mul(iszero(data.length), value)), success) { if iszero(extcodesize(target)) { revert(0x00, 0x00) } } @@ -493,7 +493,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte let success := call(gas(), target, value, ptr, data.length, codesize(), 0x00) - // prohibit sending data or zero native asset to EOAs + // prohibit sending data to EOAs; prohibit sending zero value to EOAs if lt(or(returndatasize(), mul(iszero(data.length), value)), success) { if iszero(extcodesize(target)) { revert(0x00, 0x00) } } From dadb43c83b5b1bce6f5fd22f17d3c89fd92aa11b Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Thu, 27 Nov 2025 15:32:59 +0100 Subject: [PATCH 29/72] Use packed (ERC2098) ECDSA signature representation for `_verifySimpleSignature` --- src/CrossChainReceiverFactory.sol | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index 05db5e913..ad95244a7 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -538,18 +538,22 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte } } - function _verifySimpleSignature(bytes32 signingHash, bytes calldata rsv, address owner_) private view { + function _verifySimpleSignature(bytes32 signingHash, bytes calldata rvs, address owner_) private view { assembly ("memory-safe") { - if gt(0x41, rsv.length) { + if xor(0x40, rsv.length) { mstore(0x00, 0x4e487b71) // selector for `Panic(uint256)` mstore(0x20, 0x32) // code for array out-of-bounds revert(0x1c, 0x24) } let ptr := mload(0x40) + mstore(0x00, signingHash) - calldatacopy(0x20, rsv.offset, 0x41) - mstore(0x60, shr(0xf8, mload(0x60))) + let vs := calldataload(add(0x20, rvs.offset)) + mstore(0x20, add(0x1b, shr(0xff, vs))) // v + mstore(0x40, calldataload(rsv.offset)) // r + mstore(0x60, and(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff, vs)) // s + let recovered := mload(staticcall(gas(), 0x01, 0x00, 0x80, 0x01, 0x20)) if shl(0x60, xor(owner_, recovered)) { mstore(0x00, 0x815e1d64) // `InvalidSigner.selector` From 52576572d698ebe6332aeff44fc900766085cc0c Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Thu, 27 Nov 2025 15:34:29 +0100 Subject: [PATCH 30/72] Bug! Avoid clobbering reserved memory --- src/CrossChainReceiverFactory.sol | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index ad95244a7..a16cc054a 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -704,10 +704,11 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte bytes calldata msgData = _msgData(); assembly ("memory-safe") { let ptr := mload(0x40) - calldatacopy(0x1c, msgData.offset, msgData.length) - mstore(0x00, 0x669a7d5e) // `IMultiCall.multicall.selector` + let dst := add(0x1c, ptr) + calldatacopy(dst, msgData.offset, msgData.length) + mstore(ptr, 0x669a7d5e) // `IMultiCall.multicall.selector` - let success := call(gas(), MULTICALL_ADDRESS, 0x00, /* TODO: */ 0x1c, msgData.length, codesize(), 0x00) + let success := call(gas(), MULTICALL_ADDRESS, 0x00 /* TODO: */, dst, msgData.length, codesize(), 0x00) returndatacopy(ptr, 0x00, returndatasize()) From eaeee5e83e8a2f4950d1a13cef6f4d11d9017551 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Thu, 27 Nov 2025 15:34:46 +0100 Subject: [PATCH 31/72] Pedantic correctness in `cleanup` --- src/CrossChainReceiverFactory.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index a16cc054a..7338abf9a 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -733,7 +733,9 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte } } } else { - if (_msgSender() != owner()) { + _requireProxy(); + address msgSender = _msgSender(); + if (msgSender != address(this) && msgSender != super.owner()) { _permissionDenied(); } } From ba87f23f0b12896c1466ca3453850b974a93737f Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Fri, 28 Nov 2025 16:44:59 +0100 Subject: [PATCH 32/72] Golf --- src/CrossChainReceiverFactory.sol | 92 +++++++++++++++---------------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index 7338abf9a..e4a54dcae 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -291,9 +291,9 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte // elsewhere in the ecosystem. This also means that the sort order of the hash and the // chainid is backwards from what `_getMerkleRoot` produces, again protecting us against // extension attacks. - mstore(0x00, signingHash) + mstore(callvalue(), signingHash) mstore(0x20, chainid()) - leafHash := keccak256(0x00, 0x40) + leafHash := keccak256(callvalue(), 0x40) } } @@ -331,15 +331,15 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte assembly ("memory-safe") { // derive the deployment salt from the owner mstore(0x14, initialOwner) - mstore(returndatasize(), root) - let salt := keccak256(returndatasize(), 0x34) + mstore(callvalue(), root) + let salt := keccak256(callvalue(), 0x34) // create a minimal proxy targeting this contract - mstore(returndatasize(), proxyInitCode0) + mstore(callvalue(), proxyInitCode0) mstore(0x20, proxyInitCode1) - proxy := create2(returndatasize(), returndatasize(), 0x2e, salt) + proxy := create2(callvalue(), callvalue(), 0x2e, salt) if iszero(proxy) { - mstore(returndatasize(), 0x30116425) // selector for `DeploymentFailed()`. + mstore(callvalue(), 0x30116425) // selector for `DeploymentFailed()`. revert(0x1c, 0x04) } @@ -353,10 +353,10 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte // set the owner, or `selfdestruct` mstore(0x14, argument) - mstore(returndatasize(), selector) - if iszero(call(gas(), proxy, callvalue(), 0x10, 0x24, codesize(), returndatasize())) { + mstore(callvalue(), selector) + if iszero(call(gas(), proxy, callvalue(), 0x10, 0x24, codesize(), callvalue())) { let ptr := mload(0x40) - returndatacopy(ptr, 0x00, returndatasize()) + returndatacopy(ptr, callvalue(), returndatasize()) revert(ptr, returndatasize()) } } @@ -372,9 +372,9 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte if (token == _NATIVE) { token = _WNATIVE; assembly ("memory-safe") { - if iszero(call(gas(), token, amount, codesize(), returndatasize(), codesize(), returndatasize())) { + if iszero(call(gas(), token, amount, codesize(), callvalue(), codesize(), callvalue())) { let ptr := mload(0x40) - returndatacopy(ptr, 0x00, returndatasize()) + returndatacopy(ptr, callvalue(), returndatasize()) revert(ptr, returndatasize()) } } @@ -382,22 +382,22 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte assembly ("memory-safe") { let ptr := mload(0x40) - mstore(returndatasize(), 0x095ea7b3) // selector for `approve(address,uint256)` + mstore(callvalue(), 0x095ea7b3) // selector for `approve(address,uint256)` mstore(0x20, _PERMIT2_ADDRESS) mstore(0x40, amount) - if iszero(call(gas(), token, callvalue(), 0x1c, 0x44, returndatasize(), 0x20)) { - returndatacopy(ptr, 0x00, returndatasize()) + if iszero(call(gas(), token, callvalue(), 0x1c, 0x44, callvalue(), 0x20)) { + returndatacopy(ptr, callvalue(), returndatasize()) revert(ptr, returndatasize()) } // allow `approve` to either return `true` or empty to signal success - if iszero(or(and(eq(mload(0x00), 0x01), lt(0x1f, returndatasize())), iszero(returndatasize()))) { - mstore(0x00, 0x3e3f8f73) // selector for `ApproveFailed()` + if iszero(or(and(eq(mload(callvalue()), 0x01), lt(0x1f, returndatasize())), iszero(returndatasize()))) { + mstore(callvalue(), 0x3e3f8f73) // selector for `ApproveFailed()` revert(0x1c, 0x04) } - mstore(0x00, 0x01) - return(0x00, 0x20) + mstore(callvalue(), 0x01) + return(callvalue(), 0x20) } } @@ -412,16 +412,16 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte let ptr := mload(0x40) calldatacopy(ptr, data.offset, data.length) - let success := call(gas(), target, value, ptr, data.length, codesize(), returndatasize()) + let success := call(gas(), target, value, ptr, data.length, codesize(), callvalue()) // prohibit sending data to EOAs; prohibit sending zero value to EOAs if lt(or(returndatasize(), mul(iszero(data.length), value)), success) { - if iszero(extcodesize(target)) { revert(0x00, 0x00) } + if iszero(extcodesize(target)) { revert(callvalue(), callvalue()) } } let paddedLength := and(not(0x1f), add(0x1f, returndatasize())) - mstore(add(add(0x20, ptr), paddedLength), 0x00) - returndatacopy(add(0x40, ptr), 0x00, returndatasize()) + mstore(add(add(0x20, ptr), paddedLength), callvalue()) + returndatacopy(add(0x40, ptr), callvalue(), returndatasize()) if iszero(success) { revert(add(0x40, mload(0x40)), returndatasize()) } @@ -515,23 +515,23 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte let ptr := mload(0x40) let wordPos := shr(0x08, nonce) let bitPos := shl(and(0xff, nonce), 0x01) - mstore(0x00, 0x4fe02b44) // `ISignatureTransfer.nonceBitmap.selector` + mstore(callvalue(), 0x4fe02b44) // `ISignatureTransfer.nonceBitmap.selector` mstore(0x20, address()) mstore(0x40, wordPos) - if iszero(staticcall(gas(), _PERMIT2_ADDRESS, 0x1c, 0x44, 0x20, 0x20)) { - returndatacopy(ptr, 0x00, returndatasize()) + if iszero(staticcall(gas(), _PERMIT2_ADDRESS, 0x1c, 0x44, callvalue(), 0x20)) { + returndatacopy(ptr, callvalue(), returndatasize()) revert(ptr, returndatasize()) } - let canceledNonces := mload(returndatasize()) + let canceledNonces := mload(callvalue()) if and(canceledNonces, bitPos) { - mstore(returndatasize(), 0x756688fe) // `InvalidNonce.selector` - revert(0x3c, 0x04) + mstore(callvalue(), 0x756688fe) // `InvalidNonce.selector` + revert(0x1c, 0x04) } - mstore(0x00, 0x3ff9dcb1) // `ISignatureTransfer.invalidateUnorderedNonces.selector` + mstore(callvalue(), 0x3ff9dcb1) // `ISignatureTransfer.invalidateUnorderedNonces.selector` mstore(returndatasize(), wordPos) mstore(0x40, bitPos) - if iszero(call(gas(), _PERMIT2_ADDRESS, 0x00, 0x1c, 0x44, codesize(), 0x00)) { - returndatacopy(ptr, 0x00, returndatasize()) + if iszero(call(gas(), _PERMIT2_ADDRESS, callvalue(), 0x1c, 0x44, codesize(), callvalue())) { + returndatacopy(ptr, callvalue(), returndatasize()) revert(ptr, returndatasize()) } mstore(0x40, ptr) @@ -541,26 +541,26 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte function _verifySimpleSignature(bytes32 signingHash, bytes calldata rvs, address owner_) private view { assembly ("memory-safe") { if xor(0x40, rsv.length) { - mstore(0x00, 0x4e487b71) // selector for `Panic(uint256)` + mstore(callvalue(), 0x4e487b71) // selector for `Panic(uint256)` mstore(0x20, 0x32) // code for array out-of-bounds revert(0x1c, 0x24) } let ptr := mload(0x40) - mstore(0x00, signingHash) + mstore(callvalue(), signingHash) let vs := calldataload(add(0x20, rvs.offset)) mstore(0x20, add(0x1b, shr(0xff, vs))) // v mstore(0x40, calldataload(rsv.offset)) // r mstore(0x60, and(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff, vs)) // s - let recovered := mload(staticcall(gas(), 0x01, 0x00, 0x80, 0x01, 0x20)) + let recovered := mload(staticcall(gas(), 0x01, callvalue(), 0x80, 0x01, 0x20)) if shl(0x60, xor(owner_, recovered)) { - mstore(0x00, 0x815e1d64) // `InvalidSigner.selector` + mstore(callvalue(), 0x815e1d64) // `InvalidSigner.selector` revert(0x1c, 0x04) } mstore(0x40, ptr) - mstore(0x60, 0x00) + mstore(0x60, callvalue()) } } @@ -683,7 +683,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte } if (!_verifyDeploymentRootHash(_getMerkleRoot(proof, _hashLeaf(signingHash)), owner_)) { assembly ("memory-safe") { - mstore(0x00, 0x815e1d64) // `InvalidSigner.selector` + mstore(callvalue(), 0x815e1d64) // `InvalidSigner.selector` revert(0x1c, 0x04) } } @@ -708,9 +708,9 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte calldatacopy(dst, msgData.offset, msgData.length) mstore(ptr, 0x669a7d5e) // `IMultiCall.multicall.selector` - let success := call(gas(), MULTICALL_ADDRESS, 0x00 /* TODO: */, dst, msgData.length, codesize(), 0x00) + let success := call(gas(), MULTICALL_ADDRESS, 0x00 /* TODO: */, dst, msgData.length, codesize(), callvalue()) - returndatacopy(ptr, 0x00, returndatasize()) + returndatacopy(ptr, callvalue(), returndatasize()) if iszero(success) { revert(ptr, returndatasize()) } return(ptr, returndatasize()) @@ -727,7 +727,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte call(gas(), wnative, selfbalance(), codesize(), returndatasize(), codesize(), returndatasize()) ) { let ptr := mload(0x40) - returndatacopy(ptr, 0x00, returndatasize()) + returndatacopy(ptr, callvalue(), returndatasize()) revert(ptr, returndatasize()) } } @@ -766,7 +766,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte mstore(xor(0x20, leafSlot), calldataload(offset)) // Reuse leaf to store the hash to reduce stack operations. - leaf := keccak256(0x00, 0x40) // Hash both slots of scratch space. + leaf := keccak256(callvalue(), 0x40) // Hash both slots of scratch space. offset := add(0x20, offset) // Shift 1 word per cycle. @@ -785,13 +785,13 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte // derive creation salt mstore(0x14, originalOwner) - mstore(0x00, root) - let salt := keccak256(0x00, 0x34) + mstore(callvalue(), root) + let salt := keccak256(callvalue(), 0x34) // 0xff + factory + salt + hash(initCode) mstore(0x40, initHash) mstore(0x20, salt) - mstore(0x00, factoryWithFF) + mstore(callvalue(), factoryWithFF) let computedAddress := keccak256(0x0b, 0x55) // restore clobbered memory @@ -898,7 +898,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte result := gt(returndatasize(), shl(0x60, xor(owner_, recovered))) // Restore clobbered memory - mstore(0x60, 0x00) + mstore(0x60, callvalue()) break } // Restore clobbered memory From 2e3d9fd8211c011454a7f95d58c39abc131ed5d7 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Fri, 28 Nov 2025 16:57:03 +0100 Subject: [PATCH 33/72] Typo --- src/CrossChainReceiverFactory.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index e4a54dcae..e975c37b6 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -540,7 +540,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte function _verifySimpleSignature(bytes32 signingHash, bytes calldata rvs, address owner_) private view { assembly ("memory-safe") { - if xor(0x40, rsv.length) { + if xor(0x40, rvs.length) { mstore(callvalue(), 0x4e487b71) // selector for `Panic(uint256)` mstore(0x20, 0x32) // code for array out-of-bounds revert(0x1c, 0x24) @@ -551,7 +551,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte mstore(callvalue(), signingHash) let vs := calldataload(add(0x20, rvs.offset)) mstore(0x20, add(0x1b, shr(0xff, vs))) // v - mstore(0x40, calldataload(rsv.offset)) // r + mstore(0x40, calldataload(rvs.offset)) // r mstore(0x60, and(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff, vs)) // s let recovered := mload(staticcall(gas(), 0x01, callvalue(), 0x80, 0x01, 0x20)) From 14198ed7c113bb6e87655b453d0a3f83311eff52 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Sat, 29 Nov 2025 12:26:47 +0100 Subject: [PATCH 34/72] Resolve TODO --- src/CrossChainReceiverFactory.sol | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index e975c37b6..4eb4c1e23 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -589,7 +589,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte function _hashMultiCall(IMultiCall.Call[] calldata calls, uint256 contextdepth, uint256 nonce, uint256 deadline) private pure - returns (bytes32 structHash) + returns (bytes32 structHash, uint256 value) { assembly ("memory-safe") { let ptr := mload(0x40) @@ -621,6 +621,10 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte calldatacopy(add(0x20, ptr), src, 0x60) mstore(add(0x80, ptr), data) + // if this addition overflows, then we will revert inside `MultiCall` because we + // won't have enough value to send + value := add(mload(add(0x60, ptr)), value) + // hash the `Call` object into the `Call[]` array at `arr[i]` mstore(dst, keccak256(ptr, 0xa0)) } @@ -669,8 +673,11 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte // The upper 160 bits of the nonce encode the owner address owner_ = address(uint160(nonce >> 96)); + uint256 value; if (owner_ != address(0)) { - bytes32 signingHash = _hashEip712(_hashMultiCall(calls, contextdepth, nonce, deadlineForHashing)); + bytes32 structHash; + (structHash, value) = _hashMultiCall(calls, contextdepth, nonce, deadlineForHashing); + bytes32 signingHash = _hashEip712(structHash); bytes32[] calldata proof; assembly ("memory-safe") { @@ -695,7 +702,9 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte owner_ = super.owner(); nonce |= uint256(uint160(owner_)) << 96; - bytes32 signingHash = _hashEip712(_hashMultiCall(calls, contextdepth, nonce, deadlineForHashing)); + bytes32 structHash; + (structHash, value) = _hashMultiCall(calls, contextdepth, nonce, deadlineForHashing); + bytes32 signingHash = _hashEip712(structHash); _verifySimpleSignature(signingHash, signature, owner_); } @@ -708,7 +717,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte calldatacopy(dst, msgData.offset, msgData.length) mstore(ptr, 0x669a7d5e) // `IMultiCall.multicall.selector` - let success := call(gas(), MULTICALL_ADDRESS, 0x00 /* TODO: */, dst, msgData.length, codesize(), callvalue()) + let success := call(gas(), MULTICALL_ADDRESS, value, dst, msgData.length, codesize(), callvalue()) returndatacopy(ptr, callvalue(), returndatasize()) From 1e9717402bbda47dacdbe4515de8dba47813d7d9 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Sat, 29 Nov 2025 12:27:12 +0100 Subject: [PATCH 35/72] Golf --- src/CrossChainReceiverFactory.sol | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index 4eb4c1e23..5cda2ff79 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -571,9 +571,8 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte mstore(0x20, _NAMEHASH) mstore(0x40, chainid()) mstore(0x60, address()) - let domain := keccak256(0x00, 0x80) + mstore(0x20, keccak256(0x00, 0x80)) mstore(0x00, 0x1901) - mstore(0x20, domain) mstore(0x40, structHash) signingHash := keccak256(0x1e, 0x42) mstore(0x40, ptr) From c83523659c3bf4b28393e80e54e7eaff97dbee32 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Sat, 29 Nov 2025 12:27:20 +0100 Subject: [PATCH 36/72] Compilation error --- src/CrossChainReceiverFactory.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index 5cda2ff79..0bc76c150 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -752,7 +752,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte /// Modified from Solady (https://github.com/Vectorized/solady/blob/b609a9c79ce541c2beca7a7d247665e7c93942a3/src/utils/MerkleProofLib.sol) /// Modified from Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/MerkleProofLib.sol) - function _getMerkleRoot(bytes32[] calldata proof, bytes32 leaf) private pure returns (bytes32 root) { + function _getMerkleRoot(bytes32[] calldata proof, bytes32 leaf) private view returns (bytes32 root) { assembly ("memory-safe") { if proof.length { // Left shifting by 5 is like multiplying by 32. From 1cbf80b161405ada656a251631bf823e8f4ec19c Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Mon, 1 Dec 2025 10:43:53 +0100 Subject: [PATCH 37/72] Bug! Don't revert with overflow when `ppm = 0` --- src/CrossChainReceiverFactory.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index 0bc76c150..1a5ed69d9 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -462,7 +462,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte let thisBalance := mload(0x00) patchBytes := mul(ppm, thisBalance) - if xor(div(patchBytes, ppm), thisBalance) { + if iszero(or(iszero(ppm), eq(div(patchBytes, ppm), thisBalance))) { mstore(0x00, 0x4e487b71) // selector for `Panic(uint256)` mstore(0x20, 0x11) // code for arithmetic overflow revert(0x1c, 0x24) @@ -475,7 +475,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte } patchBytes := mul(ppm, selfbalance()) - if xor(div(patchBytes, ppm), selfbalance()) { + if iszero(or(iszero(ppm), eq(div(patchBytes, ppm), selfbalance()))) { mstore(0x00, 0x4e487b71) // selector for `Panic(uint256)` mstore(0x20, 0x11) // code for arithmetic overflow revert(0x1c, 0x24) From a32cbf478b869253dd4a79aa7c030700c5db9552 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Mon, 1 Dec 2025 10:44:40 +0100 Subject: [PATCH 38/72] Unwrap `WNATIVE` to cover the `value` cost of a `metaTx` when required --- src/CrossChainReceiverFactory.sol | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index 1a5ed69d9..5b3ea6590 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -710,7 +710,19 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte _useUnorderedNonce(nonce); bytes calldata msgData = _msgData(); + IWrappedNative wnative = _WNATIVE; assembly ("memory-safe") { + if lt(selfbalance(), value) { + mstore(callvalue(), 0x2e1a7d4d) // IWrappedNative.withdraw.selector + mstore(0x20, sub(value, selfbalance())) + + if iszero(call(gas(), wnative, callvalue(), 0x1c, 0x24, codesize(), callvalue())) { + let ptr_ := mload(0x40) + returndatacopy(ptr_, callvalue(), returndatasize()) + revert(callvalue(), returndatasize()) + } + } + let ptr := mload(0x40) let dst := add(0x1c, ptr) calldatacopy(dst, msgData.offset, msgData.length) From 5cb40eeac889b01fb5fabdb2552a927cf3cf2fc1 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Mon, 1 Dec 2025 10:46:31 +0100 Subject: [PATCH 39/72] Retrieve any unspent native value from `MultiCall` after a `metaTx` --- src/CrossChainReceiverFactory.sol | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index 5b3ea6590..e986884b9 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -733,6 +733,28 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte returndatacopy(ptr, callvalue(), returndatasize()) if iszero(success) { revert(ptr, returndatasize()) } + + let multicallBalance := balance(MULTICALL_ADDRESS) + if multicallBalance { + // get any excess native value back out of `MultiCall` + mstore(add(0x20, ptr), 0x40) // calls.offset + mstore(add(0x40, ptr), callvalue()) // contextdepth (ignored because we set `revertPolicy = REVERT`) + mstore(add(0x60, ptr), 0x01) // calls.length + mstore(add(0x80, ptr), 0x20) // calls[0].offset + mstore(add(0xa0, ptr), address()) // calls[0].target + mstore(add(0xc0, ptr), callvalue()) // calls[0].revertPolicy = RevertPolicy.REVERT + mstore(add(0xe0, ptr), multicallBalance) // calls[0].value + mstore(add(0x100, ptr), 0x80) // calls[0].data.offset + mstore(add(0x120, ptr), callvalue()) // calls[0].data.length + + if iszero(call(gas(), MULTICALL_ADDRESS, callvalue(), dst, 0x124, codesize(), callvalue())) { + // this should never happen + let ptr_ := mload(0x40) + returndatacopy(ptr, callvalue(), returndatasize()) + revert(ptr, returndatasize()) + } + } + return(ptr, returndatasize()) } } From 1a643e6962110b280f4de5f328463592f13e88c2 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Mon, 1 Dec 2025 11:20:46 +0100 Subject: [PATCH 40/72] Typos --- src/CrossChainReceiverFactory.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index e986884b9..1d84b1076 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -719,7 +719,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte if iszero(call(gas(), wnative, callvalue(), 0x1c, 0x24, codesize(), callvalue())) { let ptr_ := mload(0x40) returndatacopy(ptr_, callvalue(), returndatasize()) - revert(callvalue(), returndatasize()) + revert(ptr_, returndatasize()) } } @@ -750,8 +750,8 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte if iszero(call(gas(), MULTICALL_ADDRESS, callvalue(), dst, 0x124, codesize(), callvalue())) { // this should never happen let ptr_ := mload(0x40) - returndatacopy(ptr, callvalue(), returndatasize()) - revert(ptr, returndatasize()) + returndatacopy(ptr_, callvalue(), returndatasize()) + revert(ptr_, returndatasize()) } } From 8077249df5a8815e188b09bee499172ee1f77f20 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Mon, 1 Dec 2025 11:25:41 +0100 Subject: [PATCH 41/72] Don't clobber the data we actually want to return when refunding native value in `metaTx` --- src/CrossChainReceiverFactory.sol | 35 +++++++++++++++++-------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index 1d84b1076..b2ff448ac 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -416,7 +416,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte // prohibit sending data to EOAs; prohibit sending zero value to EOAs if lt(or(returndatasize(), mul(iszero(data.length), value)), success) { - if iszero(extcodesize(target)) { revert(callvalue(), callvalue()) } + if iszero(extcodesize(target)) { revert(codesize(), callvalue()) } } let paddedLength := and(not(0x1f), add(0x1f, returndatasize())) @@ -734,28 +734,31 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte if iszero(success) { revert(ptr, returndatasize()) } + let rds := returndatasize() + let multicallBalance := balance(MULTICALL_ADDRESS) if multicallBalance { // get any excess native value back out of `MultiCall` - mstore(add(0x20, ptr), 0x40) // calls.offset - mstore(add(0x40, ptr), callvalue()) // contextdepth (ignored because we set `revertPolicy = REVERT`) - mstore(add(0x60, ptr), 0x01) // calls.length - mstore(add(0x80, ptr), 0x20) // calls[0].offset - mstore(add(0xa0, ptr), address()) // calls[0].target - mstore(add(0xc0, ptr), callvalue()) // calls[0].revertPolicy = RevertPolicy.REVERT - mstore(add(0xe0, ptr), multicallBalance) // calls[0].value - mstore(add(0x100, ptr), 0x80) // calls[0].data.offset - mstore(add(0x120, ptr), callvalue()) // calls[0].data.length - - if iszero(call(gas(), MULTICALL_ADDRESS, callvalue(), dst, 0x124, codesize(), callvalue())) { + + let ptr_ := add(ptr, returndatasize()) + mstore(ptr_, 0x669a7d5e) // `IMultiCall.multicall.selector` + mstore(add(0x20, ptr_), 0x40) // calls.offset + mstore(add(0x40, ptr_), callvalue()) // contextdepth (ignored because we set `revertPolicy = REVERT`) + mstore(add(0x60, ptr_), 0x01) // calls.length + mstore(add(0x80, ptr_), 0x20) // calls[0].offset + mstore(add(0xa0, ptr_), address()) // calls[0].target + mstore(add(0xc0, ptr_), callvalue()) // calls[0].revertPolicy = RevertPolicy.REVERT + mstore(add(0xe0, ptr_), multicallBalance) // calls[0].value + mstore(add(0x100, ptr_), 0x80) // calls[0].data.offset + mstore(add(0x120, ptr_), callvalue()) // calls[0].data.length + + if iszero(call(gas(), MULTICALL_ADDRESS, callvalue(), add(0x1c, ptr_), 0x124, codesize(), callvalue())) { // this should never happen - let ptr_ := mload(0x40) - returndatacopy(ptr_, callvalue(), returndatasize()) - revert(ptr_, returndatasize()) + revert(codesize(), callvalue()) } } - return(ptr, returndatasize()) + return(ptr, rds) } } From 8ff952fec47b8fee7660a18d9bb04b9eb22b0f1a Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Tue, 2 Dec 2025 10:53:26 +0100 Subject: [PATCH 42/72] WIP: use a sentinel value for `calls[i].target` when the target is the minimal proxy itself (avoid circular hashing) --- src/CrossChainReceiverFactory.sol | 163 +++++++++++++++++++----------- 1 file changed, 106 insertions(+), 57 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index b2ff448ac..568e606c7 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -49,13 +49,14 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte bytes32 private constant _NAMEHASH = 0x819c7f86c24229cd5fed5a41696eb0cd8b3f84cc632df73cfd985e8b100980e8; bytes32 private constant _DOMAIN_TYPEHASH = 0x8cad95687ba82c2ce50e74f7b754645e5117c3a5bec8151c0726d5857980a866; - // TODO: add a `value` field bytes32 private constant _MULTICALL_TYPEHASH = 0xd0290069becb7f8c7bc360deb286fb78314d4fb3e65d17004248ee046bd770a9; bytes32 private constant _CALL_TYPEHASH = 0xa8b3616b5b84550a806f58ebe7d19199754b9632d31e5e6d07e7faf21fe1cacc; address private constant _NATIVE_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; IERC20 private constant _NATIVE = IERC20(_NATIVE_ADDRESS); + address private constant _ADDRESS_THIS_SENTINEL = 0x0000000000000061646472657373287468697329; // address(uint160(uint104(bytes13("address(this)")))) + address private constant _TOEHOLD = 0x4e59b44847b379578588920cA78FbF26c0B4956C; address private constant _WNATIVE_SETTER = 0x000000000000F01B1D1c8EEF6c6cF71a0b658Fbc; bytes32 private constant _WNATIVE_STORAGE_INITHASH = keccak256( @@ -585,68 +586,112 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte // 1. The signer of the object wouldn't sign an invalid EIP712 serialization of the object // containing dirty bits // 2. The object will be used later in this context in a way that *does* check for dirty bits - function _hashMultiCall(IMultiCall.Call[] calldata calls, uint256 contextdepth, uint256 nonce, uint256 deadline) + function _hashMultiCall(bytes calldata msgData, uint256 nonce, uint256 deadline) private - pure - returns (bytes32 structHash, uint256 value) + view + returns (bytes32 structHash, bytes memory data, uint256 totalValue) { assembly ("memory-safe") { - let ptr := mload(0x40) - let lenBytes := shl(0x05, calls.length) - let arr := add(0xa0, ptr) + data := mload(0x40) + mstore(data, msgData.length) + let calls := add(0x20, data) + + let scratch + { + let argsLength := sub(msgData.length, 0x04) + calldatacopy(calls, add(0x04, msgData.offset), argsLength) + scratch := add(calls, argsLength) + } + mstore(0x40, scratch) + let lastWord := sub(scratch, 0x20) + + let err + { + let offset := mload(calls) + let oom := shr(0x40, offset) + calls := add(offset, calls) + err := or(lt(lastWord, calls), or(oom, err)) + } + + let callsLengthBytes + { + let callsLength := mload(calls) + let oom := shr(0x3b, callsLength) + callsLengthBytes := shl(0x05, callsLength) + err := or(lt(lastWord, add(calls, callsLengthBytes)), or(oom, err)) + } + calls := add(0x20, calls) + for { let i - mstore(ptr, _CALL_TYPEHASH) - } xor(i, lenBytes) { i := add(0x20, i) } { - let dst := add(i, arr) - let src := add(i, calls.offset) + } xor(i, callsLengthBytes) { i := add(0x20, i) } { + let dst := add(i, scratch) + let src := add(i, calls) // indirect `src` because it points to a dynamic type - src := add(calls.offset, calldataload(src)) + { + let offset := mload(src) + let oom := shr(0x40, offset) + src := add(calls, offset) + err := or(lt(lastWord, add(0x60, src)), or(oom, err)) + } // indirect `src.data` because it also points to a dynamic type - let data := add(0x60, src) - data := add(calldataload(data), src) + let srcData + { + let offset := mload(add(0x60, src)) + let oom := shr(0x40, offset) + srcData := add(src, offset) + err := or(lt(lastWord, srcData), or(oom, err)) + } // decode the length of `src.data` and hash it { - let dataLength := calldataload(data) - data := add(0x20, data) - calldatacopy(dst, data, dataLength) - data := keccak256(dst, dataLength) + let srcDataLength := mload(srcData) + let oom := shr(0x40, srcDataLength) + err := or(lt(lastWord, add(srcData, srcDataLength)), or(oom, err)) + srcData := keccak256(add(0x20, srcData), srcDataLength) } - // EIP712-encode the fixed-length fields of the `Call` object - calldatacopy(add(0x20, ptr), src, 0x60) - mstore(add(0x80, ptr), data) + // hash the `Call` object into the `Call[]` array at `scratch[i]` + let typeHashWord := sub(src, 0x20) + let typeHashWordValue := mload(typeHashWord) + let srcDataWord := add(0x60, src) // TODO: DRY + let srcDataWordValue := mload(srcDataWord) // TODO: DRY + mstore(typeHashWord, _CALL_TYPEHASH) + mstore(srcDataWord, srcData) + mstore(dst, keccak256(typeHashWord, 0xa0)) + mstore(typeHashWord, typeHashWordValue) + mstore(srcDataWord, srcDataWordValue) + + // replace `src.target` with `address(this)` if it is `_ADDRESS_THIS_SENTINEL` + let srcTarget := mload(src) + mstore(src, xor(srcTarget, mul(eq(_ADDRESS_THIS_SENTINEL, srcTarget), xor(address(), srcTarget)))) // if this addition overflows, then we will revert inside `MultiCall` because we // won't have enough value to send - value := add(mload(add(0x60, ptr)), value) - - // hash the `Call` object into the `Call[]` array at `arr[i]` - mstore(dst, keccak256(ptr, 0xa0)) + totalValue := add(mload(add(0x40, src)), totalValue) } // hash the `Call[]` array - arr := keccak256(arr, lenBytes) + let callsHash := keccak256(scratch, callsLengthBytes) // EIP712-encode the `MultiCall` object - mstore(ptr, _MULTICALL_TYPEHASH) - mstore(add(0x20, ptr), arr) - mstore(add(0x40, ptr), contextdepth) - mstore(add(0x60, ptr), nonce) - mstore(add(0x80, ptr), deadline) + mstore(scratch, _MULTICALL_TYPEHASH) + mstore(add(0x20, scratch), callsHash) + mstore(add(0x40, scratch), mload(add(0x20, calls))) + mstore(add(0x60, scratch), nonce) + mstore(add(0x80, scratch), deadline) // final hashing - structHash := keccak256(ptr, 0xa0) + structHash := keccak256(scratch, 0xa0) } } /// @inheritdoc ICrossChainReceiverFactory function metaTx( - IMultiCall.Call[] calldata calls, - uint256 contextdepth, + IMultiCall.Call[] calldata /* calls */, + uint256 /* contextdepth */, uint256 nonce, uint256 deadline, bytes calldata signature @@ -672,10 +717,11 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte // The upper 160 bits of the nonce encode the owner address owner_ = address(uint160(nonce >> 96)); + bytes memory data; uint256 value; if (owner_ != address(0)) { bytes32 structHash; - (structHash, value) = _hashMultiCall(calls, contextdepth, nonce, deadlineForHashing); + (structHash, data, value) = _hashMultiCall(_msgData(), nonce, deadlineForHashing); bytes32 signingHash = _hashEip712(structHash); bytes32[] calldata proof; @@ -702,7 +748,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte nonce |= uint256(uint160(owner_)) << 96; bytes32 structHash; - (structHash, value) = _hashMultiCall(calls, contextdepth, nonce, deadlineForHashing); + (structHash, data, value) = _hashMultiCall(_msgData(), nonce, deadlineForHashing); bytes32 signingHash = _hashEip712(structHash); _verifySimpleSignature(signingHash, signature, owner_); } @@ -723,16 +769,19 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte } } - let ptr := mload(0x40) - let dst := add(0x1c, ptr) - calldatacopy(dst, msgData.offset, msgData.length) - mstore(ptr, 0x669a7d5e) // `IMultiCall.multicall.selector` + let dataLength := mload(data) + mstore(data, 0x669a7d5e) // `IMultiCall.multicall.selector` + // we won't bother to restore `data.length` because this block never returns to Solidity - let success := call(gas(), MULTICALL_ADDRESS, value, dst, msgData.length, codesize(), callvalue()) + let success := call(gas(), MULTICALL_ADDRESS, value, add(0x1c, data), msgData.length, codesize(), callvalue()) - returndatacopy(ptr, callvalue(), returndatasize()) + // technically, this is not memory safe because there could be a hidden + // compiler-allocated object at the end of `data` and the returndata from the `CALL` + // could exceed `data.length`. in practice however, this is not a thing that happens, so + // it's fine. + returndatacopy(data, callvalue(), returndatasize()) - if iszero(success) { revert(ptr, returndatasize()) } + if iszero(success) { revert(data, returndatasize()) } let rds := returndatasize() @@ -740,25 +789,25 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte if multicallBalance { // get any excess native value back out of `MultiCall` - let ptr_ := add(ptr, returndatasize()) - mstore(ptr_, 0x669a7d5e) // `IMultiCall.multicall.selector` - mstore(add(0x20, ptr_), 0x40) // calls.offset - mstore(add(0x40, ptr_), callvalue()) // contextdepth (ignored because we set `revertPolicy = REVERT`) - mstore(add(0x60, ptr_), 0x01) // calls.length - mstore(add(0x80, ptr_), 0x20) // calls[0].offset - mstore(add(0xa0, ptr_), address()) // calls[0].target - mstore(add(0xc0, ptr_), callvalue()) // calls[0].revertPolicy = RevertPolicy.REVERT - mstore(add(0xe0, ptr_), multicallBalance) // calls[0].value - mstore(add(0x100, ptr_), 0x80) // calls[0].data.offset - mstore(add(0x120, ptr_), callvalue()) // calls[0].data.length - - if iszero(call(gas(), MULTICALL_ADDRESS, callvalue(), add(0x1c, ptr_), 0x124, codesize(), callvalue())) { + let ptr := add(add(0x20, returndatasize()), data) + mstore(ptr, 0x669a7d5e) // `IMultiCall.multicall.selector` + mstore(add(0x20, ptr), 0x40) // calls.offset + mstore(add(0x40, ptr), callvalue()) // contextdepth (ignored because we set `revertPolicy = REVERT`) + mstore(add(0x60, ptr), 0x01) // calls.length + mstore(add(0x80, ptr), 0x20) // calls[0].offset + mstore(add(0xa0, ptr), address()) // calls[0].target + mstore(add(0xc0, ptr), callvalue()) // calls[0].revertPolicy = RevertPolicy.REVERT + mstore(add(0xe0, ptr), multicallBalance) // calls[0].value + mstore(add(0x100, ptr), 0x80) // calls[0].data.offset + mstore(add(0x120, ptr), callvalue()) // calls[0].data.length + + if iszero(call(gas(), MULTICALL_ADDRESS, callvalue(), add(0x1c, ptr), 0x124, codesize(), callvalue())) { // this should never happen revert(codesize(), callvalue()) } } - return(ptr, rds) + return(data, rds) } } From b2828bc1c513f88b7c152b89e11157e6926561df Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Tue, 2 Dec 2025 11:03:42 +0100 Subject: [PATCH 43/72] Bugs! Missing final memory bounds check revert. Loaded the wrong word as `contextdepth` --- src/CrossChainReceiverFactory.sol | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index 568e606c7..bf4515a75 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -604,7 +604,10 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte } mstore(0x40, scratch) let lastWord := sub(scratch, 0x20) + let contextdepth := mload(add(0x20, calls)) + // indirect `calls` so that it points to the beginning of the array of indirection + // pointers to individual `IMultiCall.Call` structs let err { let offset := mload(calls) @@ -653,7 +656,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte srcData := keccak256(add(0x20, srcData), srcDataLength) } - // hash the `Call` object into the `Call[]` array at `scratch[i]` + // EIP712-hash the `Call` object into the `Call[]` array at `scratch[i]` let typeHashWord := sub(src, 0x20) let typeHashWordValue := mload(typeHashWord) let srcDataWord := add(0x60, src) // TODO: DRY @@ -673,13 +676,15 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte totalValue := add(mload(add(0x40, src)), totalValue) } + if err { revert(codesize(), callvalue()) } + // hash the `Call[]` array let callsHash := keccak256(scratch, callsLengthBytes) // EIP712-encode the `MultiCall` object mstore(scratch, _MULTICALL_TYPEHASH) mstore(add(0x20, scratch), callsHash) - mstore(add(0x40, scratch), mload(add(0x20, calls))) + mstore(add(0x40, scratch), contextdepth) mstore(add(0x60, scratch), nonce) mstore(add(0x80, scratch), deadline) From 10d33570e9c2ed00bbdadfa5f86c8131c7f46e5a Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Tue, 2 Dec 2025 11:55:01 +0100 Subject: [PATCH 44/72] Comments --- src/CrossChainReceiverFactory.sol | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index bf4515a75..fdf0dbc81 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -657,7 +657,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte } // EIP712-hash the `Call` object into the `Call[]` array at `scratch[i]` - let typeHashWord := sub(src, 0x20) + let typeHashWord := sub(src, 0x20) // not technically memory safe let typeHashWordValue := mload(typeHashWord) let srcDataWord := add(0x60, src) // TODO: DRY let srcDataWordValue := mload(srcDataWord) // TODO: DRY @@ -671,8 +671,9 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte let srcTarget := mload(src) mstore(src, xor(srcTarget, mul(eq(_ADDRESS_THIS_SENTINEL, srcTarget), xor(address(), srcTarget)))) - // if this addition overflows, then we will revert inside `MultiCall` because we - // won't have enough value to send + // if this addition overflows, then the call will fail inside `MultiCall` because we + // won't have enough value to send. depending on the value of `revertPolicy` this + // could be a GIGO error or cause the `multicall` to revert. totalValue := add(mload(add(0x40, src)), totalValue) } From a6736ec2a47cdaeea84bfb0a174161a3bedfa59c Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Tue, 2 Dec 2025 11:59:24 +0100 Subject: [PATCH 45/72] Don't revert if there isn't enough value to unwrap; just unwrap everything and move on --- src/CrossChainReceiverFactory.sol | 45 ++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index fdf0dbc81..385ca30d3 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -761,25 +761,46 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte _useUnorderedNonce(nonce); - bytes calldata msgData = _msgData(); - IWrappedNative wnative = _WNATIVE; - assembly ("memory-safe") { - if lt(selfbalance(), value) { - mstore(callvalue(), 0x2e1a7d4d) // IWrappedNative.withdraw.selector - mstore(0x20, sub(value, selfbalance())) - - if iszero(call(gas(), wnative, callvalue(), 0x1c, 0x24, codesize(), callvalue())) { - let ptr_ := mload(0x40) - returndatacopy(ptr_, callvalue(), returndatasize()) - revert(ptr_, returndatasize()) + unchecked { + if (address(this).balance < value) { + IWrappedNative wnative = _WNATIVE; + + uint256 wrappedBalance; + assembly ("memory-safe") { + mstore(0x00, 0x70a08231) // `IERC20.balanceOf.selector` + mstore(0x20, address()) + + if iszero(staticcall(gas(), wnative, 0x1c, 0x24, callvalue(), 0x20)) { + let ptr := mload(0x40) + returndatacopy(ptr, callvalue(), returndatasize()) + revert(ptr, returndatasize()) + } + + wrappedBalance := mload(callvalue()) + } + + uint256 toUnwrap = (address(this).balance + wrappedBalance < value).ternary(wrappedBalance, value - address(this).balance); + value = toUnwrap + address(this).balance; + + assembly ("memory-safe") { + mstore(callvalue(), 0x2e1a7d4d) // IWrappedNative.withdraw.selector + mstore(0x20, toUnwrap) + + if iszero(call(gas(), wnative, callvalue(), 0x1c, 0x24, codesize(), callvalue())) { + let ptr := mload(0x40) + returndatacopy(ptr, callvalue(), returndatasize()) + revert(ptr, returndatasize()) + } } } + } + assembly ("memory-safe") { let dataLength := mload(data) mstore(data, 0x669a7d5e) // `IMultiCall.multicall.selector` // we won't bother to restore `data.length` because this block never returns to Solidity - let success := call(gas(), MULTICALL_ADDRESS, value, add(0x1c, data), msgData.length, codesize(), callvalue()) + let success := call(gas(), MULTICALL_ADDRESS, value, add(0x1c, data), dataLength, codesize(), callvalue()) // technically, this is not memory safe because there could be a hidden // compiler-allocated object at the end of `data` and the returndata from the `CALL` From 9713df362a6d92e34307ebfb4e960ea0d931236d Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Tue, 2 Dec 2025 12:02:21 +0100 Subject: [PATCH 46/72] `forge fmt` --- src/CrossChainReceiverFactory.sol | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index 385ca30d3..ebf41257b 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -625,9 +625,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte } calls := add(0x20, calls) - for { - let i - } xor(i, callsLengthBytes) { i := add(0x20, i) } { + for { let i } xor(i, callsLengthBytes) { i := add(0x20, i) } { let dst := add(i, scratch) let src := add(i, calls) @@ -779,7 +777,9 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte wrappedBalance := mload(callvalue()) } - uint256 toUnwrap = (address(this).balance + wrappedBalance < value).ternary(wrappedBalance, value - address(this).balance); + uint256 toUnwrap = (address(this).balance + wrappedBalance < value).ternary( + wrappedBalance, value - address(this).balance + ); value = toUnwrap + address(this).balance; assembly ("memory-safe") { From d9378c7101ce297e0ff41a8523d476604a8c432e Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Tue, 2 Dec 2025 12:15:03 +0100 Subject: [PATCH 47/72] Comments; function name --- src/CrossChainReceiverFactory.sol | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index ebf41257b..90a053588 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -565,7 +565,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte } } - function _hashEip712(bytes32 structHash) private view returns (bytes32 signingHash) { + function _eip712SigningHash(bytes32 structHash) private view returns (bytes32 signingHash) { assembly ("memory-safe") { let ptr := mload(0x40) mstore(0x00, _DOMAIN_TYPEHASH) @@ -585,13 +585,20 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte // that: // 1. The signer of the object wouldn't sign an invalid EIP712 serialization of the object // containing dirty bits - // 2. The object will be used later in this context in a way that *does* check for dirty bits + // 2. The object will be used later in a way that *does* check for dirty bits and causes a + // revert function _hashMultiCall(bytes calldata msgData, uint256 nonce, uint256 deadline) private view returns (bytes32 structHash, bytes memory data, uint256 totalValue) { assembly ("memory-safe") { + // reencoding the `calls` argument or even just following the indirection pointer to the + // encoded array of offsets and attempting to copy/forward only that portion of the + // calldata is more complex and gas-expensive than just copying the whole thing + // (including the signature) and forwarding it to `MultiCall`, so there will be a bunch + // of extra garbage included with our call to `MultiCall.multicall` that is ignored when + // that function decodes it. data := mload(0x40) mstore(data, msgData.length) let calls := add(0x20, data) @@ -726,7 +733,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte if (owner_ != address(0)) { bytes32 structHash; (structHash, data, value) = _hashMultiCall(_msgData(), nonce, deadlineForHashing); - bytes32 signingHash = _hashEip712(structHash); + bytes32 signingHash = _eip712SigningHash(structHash); bytes32[] calldata proof; assembly ("memory-safe") { @@ -753,7 +760,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte bytes32 structHash; (structHash, data, value) = _hashMultiCall(_msgData(), nonce, deadlineForHashing); - bytes32 signingHash = _hashEip712(structHash); + bytes32 signingHash = _eip712SigningHash(structHash); _verifySimpleSignature(signingHash, signature, owner_); } From 3cf9b937f0ed9308effdfc5bd0932deaa4d92ad8 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Tue, 2 Dec 2025 18:34:08 +0100 Subject: [PATCH 48/72] Bug! Break *another* hash cycle in Merkle proof `metaTx` flows --- src/CrossChainReceiverFactory.sol | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index 90a053588..c90824a42 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -48,6 +48,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte string public constant override name = "ZeroExCrossChainReceiver"; bytes32 private constant _NAMEHASH = 0x819c7f86c24229cd5fed5a41696eb0cd8b3f84cc632df73cfd985e8b100980e8; bytes32 private constant _DOMAIN_TYPEHASH = 0x8cad95687ba82c2ce50e74f7b754645e5117c3a5bec8151c0726d5857980a866; + bytes32 private constant _SENTINEL_DOMAIN_SEPARATOR = 0x645883bdca79cf2f0cd9e1ce41a5e705279b61c531a89508da475b856926949a; bytes32 private constant _MULTICALL_TYPEHASH = 0xd0290069becb7f8c7bc360deb286fb78314d4fb3e65d17004248ee046bd770a9; bytes32 private constant _CALL_TYPEHASH = 0xa8b3616b5b84550a806f58ebe7d19199754b9632d31e5e6d07e7faf21fe1cacc; @@ -121,6 +122,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte require(uint160(_WNATIVE_SETTER) >> 112 == 0); require(_NAMEHASH == keccak256(bytes(name))); require(_DOMAIN_TYPEHASH == keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)")); + require(_SENTINEL_DOMAIN_SEPARATOR == keccak256(abi.encode(keccak256("EIP712Domain(string name,address verifyingContract)"), _NAMEHASH, _ADDRESS_THIS_SENTINEL))); require( _MULTICALL_TYPEHASH == keccak256( @@ -581,6 +583,24 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte } } + // This function is provided for use with the Merkle proof flows inside `metaTx` where we can't + // combine the EIP712 struct hash with the domain separator to derive the leaf of the tree + // because the domain separator hashes over the address of the proxy, which is determined by the + // root of the tree. This function breaks the hash cycle by substituting the sentinel for the + // address of the proxy in the domain separator. For gas efficiency, we also omit the chainid + // from the domain separator because the signing hash is first hashed with the chainid before + // forming the Merkle leaf. + function _nonEip712SigningHash(bytes32 structHash) private pure returns (bytes32 signingHash) { + assembly ("memory-safe") { + let ptr := mload(0x40) + mstore(0x00, 0x1901) + mstore(0x20, _SENTINEL_DOMAIN_SEPARATOR) + mstore(0x40, structHash) + signingHash := keccak256(0x1e, 0x42) + mstore(0x40, ptr) + } + } + // This function intentionally ignores any dirty bits that might be present in `calls`, assuming // that: // 1. The signer of the object wouldn't sign an invalid EIP712 serialization of the object @@ -733,7 +753,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte if (owner_ != address(0)) { bytes32 structHash; (structHash, data, value) = _hashMultiCall(_msgData(), nonce, deadlineForHashing); - bytes32 signingHash = _eip712SigningHash(structHash); + bytes32 signingHash = _nonEip712SigningHash(structHash); bytes32[] calldata proof; assembly ("memory-safe") { From e6c9fc777ec2aae6d9964efac91c029faba9a689 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Tue, 2 Dec 2025 18:36:26 +0100 Subject: [PATCH 49/72] `forge fmt` --- src/CrossChainReceiverFactory.sol | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index c90824a42..f877cc6ce 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -48,7 +48,8 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte string public constant override name = "ZeroExCrossChainReceiver"; bytes32 private constant _NAMEHASH = 0x819c7f86c24229cd5fed5a41696eb0cd8b3f84cc632df73cfd985e8b100980e8; bytes32 private constant _DOMAIN_TYPEHASH = 0x8cad95687ba82c2ce50e74f7b754645e5117c3a5bec8151c0726d5857980a866; - bytes32 private constant _SENTINEL_DOMAIN_SEPARATOR = 0x645883bdca79cf2f0cd9e1ce41a5e705279b61c531a89508da475b856926949a; + bytes32 private constant _SENTINEL_DOMAIN_SEPARATOR = + 0x645883bdca79cf2f0cd9e1ce41a5e705279b61c531a89508da475b856926949a; bytes32 private constant _MULTICALL_TYPEHASH = 0xd0290069becb7f8c7bc360deb286fb78314d4fb3e65d17004248ee046bd770a9; bytes32 private constant _CALL_TYPEHASH = 0xa8b3616b5b84550a806f58ebe7d19199754b9632d31e5e6d07e7faf21fe1cacc; @@ -122,7 +123,14 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte require(uint160(_WNATIVE_SETTER) >> 112 == 0); require(_NAMEHASH == keccak256(bytes(name))); require(_DOMAIN_TYPEHASH == keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)")); - require(_SENTINEL_DOMAIN_SEPARATOR == keccak256(abi.encode(keccak256("EIP712Domain(string name,address verifyingContract)"), _NAMEHASH, _ADDRESS_THIS_SENTINEL))); + require( + _SENTINEL_DOMAIN_SEPARATOR + == keccak256( + abi.encode( + keccak256("EIP712Domain(string name,address verifyingContract)"), _NAMEHASH, _ADDRESS_THIS_SENTINEL + ) + ) + ); require( _MULTICALL_TYPEHASH == keccak256( From 6faae4b735a32fae759f4304c5a28295196ba4ab Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Tue, 2 Dec 2025 19:14:37 +0100 Subject: [PATCH 50/72] Golf --- src/CrossChainReceiverFactory.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index f877cc6ce..ce7374838 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -578,12 +578,12 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte function _eip712SigningHash(bytes32 structHash) private view returns (bytes32 signingHash) { assembly ("memory-safe") { let ptr := mload(0x40) - mstore(0x00, _DOMAIN_TYPEHASH) + mstore(callvalue(), _DOMAIN_TYPEHASH) mstore(0x20, _NAMEHASH) mstore(0x40, chainid()) mstore(0x60, address()) mstore(0x20, keccak256(0x00, 0x80)) - mstore(0x00, 0x1901) + mstore(callvalue(), 0x1901) mstore(0x40, structHash) signingHash := keccak256(0x1e, 0x42) mstore(0x40, ptr) @@ -601,7 +601,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte function _nonEip712SigningHash(bytes32 structHash) private pure returns (bytes32 signingHash) { assembly ("memory-safe") { let ptr := mload(0x40) - mstore(0x00, 0x1901) + mstore(callvalue(), 0x1901) mstore(0x20, _SENTINEL_DOMAIN_SEPARATOR) mstore(0x40, structHash) signingHash := keccak256(0x1e, 0x42) From 6b10d1d78277b64e00a5cc9bf11948ff68c530df Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Tue, 2 Dec 2025 22:26:31 +0100 Subject: [PATCH 51/72] Compilation error --- src/CrossChainReceiverFactory.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index ce7374838..8de3a973f 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -598,7 +598,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte // address of the proxy in the domain separator. For gas efficiency, we also omit the chainid // from the domain separator because the signing hash is first hashed with the chainid before // forming the Merkle leaf. - function _nonEip712SigningHash(bytes32 structHash) private pure returns (bytes32 signingHash) { + function _nonEip712SigningHash(bytes32 structHash) private view returns (bytes32 signingHash) { assembly ("memory-safe") { let ptr := mload(0x40) mstore(callvalue(), 0x1901) From 8734ae2ae577db0f4cb2a73ad02c7e1c6b8ffcd7 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Wed, 3 Dec 2025 00:32:48 +0100 Subject: [PATCH 52/72] Resolve TODO --- src/CrossChainReceiverFactory.sol | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index 8de3a973f..6aa194c24 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -451,11 +451,13 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte returns (bytes memory) { assembly ("memory-safe") { - // TODO: allow sending empty data with `offset == 0` if `token == _NATIVE` + // empty data with offset == 0 is OK. otherwise, perform bounds checking if iszero(lt(add(0x1f, patchOffset), data.length)) { - mstore(0x00, 0x4e487b71) // selector for `Panic(uint256)` - mstore(0x20, 0x32) // code for array out-of-bounds - revert(0x1c, 0x24) + if or(or(data.length, patchOffset), shl(0x60, xor(_NATIVE_ADDRESS, token))) { + mstore(0x00, 0x4e487b71) // selector for `Panic(uint256)` + mstore(0x20, 0x32) // code for array out-of-bounds + revert(0x1c, 0x24) + } } let patchBytes From 17bc8059a446a5651a81397faa895d0902728e26 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Wed, 3 Dec 2025 00:33:23 +0100 Subject: [PATCH 53/72] Golf --- src/CrossChainReceiverFactory.sol | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index 6aa194c24..74383a224 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -453,7 +453,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte assembly ("memory-safe") { // empty data with offset == 0 is OK. otherwise, perform bounds checking if iszero(lt(add(0x1f, patchOffset), data.length)) { - if or(or(data.length, patchOffset), shl(0x60, xor(_NATIVE_ADDRESS, token))) { + if or(shl(0x60, xor(_NATIVE_ADDRESS, token)), or(data.length, patchOffset)) { mstore(0x00, 0x4e487b71) // selector for `Panic(uint256)` mstore(0x20, 0x32) // code for array out-of-bounds revert(0x1c, 0x24) @@ -676,10 +676,13 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte // indirect `src.data` because it also points to a dynamic type let srcData + let srcDataWord + let srcDataWordValue { - let offset := mload(add(0x60, src)) - let oom := shr(0x40, offset) - srcData := add(src, offset) + srcDataWord := add(0x60, src) + srcDataWordValue := mload(srcDataWord) + let oom := shr(0x40, srcDataWordValue) + srcData := add(src, srcDataWordValue) err := or(lt(lastWord, srcData), or(oom, err)) } @@ -694,8 +697,6 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte // EIP712-hash the `Call` object into the `Call[]` array at `scratch[i]` let typeHashWord := sub(src, 0x20) // not technically memory safe let typeHashWordValue := mload(typeHashWord) - let srcDataWord := add(0x60, src) // TODO: DRY - let srcDataWordValue := mload(srcDataWord) // TODO: DRY mstore(typeHashWord, _CALL_TYPEHASH) mstore(srcDataWord, srcData) mstore(dst, keccak256(typeHashWord, 0xa0)) From 09e86a3b922c974faae1f2996d62502867be1269 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Fri, 5 Dec 2025 18:26:03 +0100 Subject: [PATCH 54/72] Trim some code size because we know `WNATIVE` is well-behaved --- src/CrossChainReceiverFactory.sol | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index 74383a224..ac67f3e31 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -807,9 +807,8 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte mstore(0x20, address()) if iszero(staticcall(gas(), wnative, 0x1c, 0x24, callvalue(), 0x20)) { - let ptr := mload(0x40) - returndatacopy(ptr, callvalue(), returndatasize()) - revert(ptr, returndatasize()) + // this should never happen + revert(codesize(), callvalue()) } wrappedBalance := mload(callvalue()) @@ -825,9 +824,8 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte mstore(0x20, toUnwrap) if iszero(call(gas(), wnative, callvalue(), 0x1c, 0x24, codesize(), callvalue())) { - let ptr := mload(0x40) - returndatacopy(ptr, callvalue(), returndatasize()) - revert(ptr, returndatasize()) + // this should never happen + revert(codesize(), callvalue()) } } } @@ -885,9 +883,8 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte if iszero( call(gas(), wnative, selfbalance(), codesize(), returndatasize(), codesize(), returndatasize()) ) { - let ptr := mload(0x40) - returndatacopy(ptr, callvalue(), returndatasize()) - revert(ptr, returndatasize()) + // this should never happen + revert(codesize(), callvalue()) } } } @@ -1071,9 +1068,8 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte assembly ("memory-safe") { if iszero(call(gas(), wnative, callvalue(), codesize(), returndatasize(), codesize(), returndatasize())) { - let ptr := mload(0x40) - returndatacopy(ptr, 0x00, returndatasize()) - revert(ptr, returndatasize()) + // this should never happen + revert(codesize(), callvalue()) } } } From 0f25c356cc524ee3cc5576ccf099415a4599e26d Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Sun, 7 Dec 2025 16:29:43 +0100 Subject: [PATCH 55/72] Implement `getFromMulticall`, assuming that at some point we will send tokens to the multicall contract and want to get them out --- src/CrossChainReceiverFactory.sol | 78 +++++++++++++++++++ src/interfaces/ICrossChainReceiverFactory.sol | 4 + 2 files changed, 82 insertions(+) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index ac67f3e31..d896d5ce0 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -412,6 +412,84 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte } } + /// @inheritdoc ICrossChainReceiverFactory + function getFromMulticall(IERC20 token, address payable recipient) external override returns (bool) { + assembly ("memory-safe") { + for {} true {} { + if shl(0x60, xor(_NATIVE_ADDRESS, token)) { + mstore(callvalue(), 0x70a08231) + mstore(0x20, MULTICALL_ADDRESS) + if iszero(staticcall(gas(), token, 0x1c, 0x24, callvalue(), 0x20)) { + let ptr_ := mload(0x40) + returndatacopy(ptr_, callvalue(), returndatasize()) + revert(ptr_, returndatasize()) + } + let amount := mload(callvalue()) + if iszero(amount) { + break + } + + let ptr := mload(0x40) + + mstore(ptr, 0x669a7d5e) // `IMultiCall.multicall.selector` + mstore(add(0x20, ptr), 0x40) // calls.offset + mstore(add(0x40, ptr), callvalue()) // contextdepth (ignored because we set `revertPolicy = REVERT`) + mstore(add(0x60, ptr), 0x01) // calls.length + mstore(add(0x80, ptr), 0x20) // calls[0].offset + mstore(add(0xa0, ptr), and(0xffffffffffffffffffffffffffffffffffffffff, token)) // calls[0].target + mstore(add(0xc0, ptr), callvalue()) // calls[0].revertPolicy = RevertPolicy.REVERT + mstore(add(0xe0, ptr), callvalue()) // calls[0].value + mstore(add(0x100, ptr), 0x80) // calls[0].data.offset + + mstore(add(0x164, ptr), amount) + mstore(add(0x144, ptr), recipient) + mstore(add(0x130, ptr), 0xa9059cbb) // `IERC20.transfer.selector` with `recipient`'s padding + + mstore(add(0x120, ptr), 0x44) // calls[0].data.length + + if iszero(call(gas(), MULTICALL_ADDRESS, callvalue(), add(0x1c, ptr), 0x168, codesize(), callvalue())) { + let ptr_ := mload(0x40) + returndatacopy(ptr_, callvalue(), returndatasize()) + revert(ptr_, returndatasize()) + } + + break + } + + { + let amount := balance(MULTICALL_ADDRESS) + if iszero(amount) { + break + } + + let ptr := mload(0x40) + + mstore(ptr, 0x669a7d5e) // `IMultiCall.multicall.selector` + mstore(add(0x20, ptr), 0x40) // calls.offset + mstore(add(0x40, ptr), callvalue()) // contextdepth (ignored because we set `revertPolicy = REVERT`) + mstore(add(0x60, ptr), 0x01) // calls.length + mstore(add(0x80, ptr), 0x20) // calls[0].offset + mstore(add(0xa0, ptr), and(0xffffffffffffffffffffffffffffffffffffffff, recipient)) // calls[0].target + mstore(add(0xc0, ptr), callvalue()) // calls[0].revertPolicy = RevertPolicy.REVERT + mstore(add(0xe0, ptr), amount) // calls[0].value + mstore(add(0x100, ptr), 0x80) // calls[0].data.offset + mstore(add(0x120, ptr), callvalue()) // calls[0].data.length + + if iszero(call(gas(), MULTICALL_ADDRESS, callvalue(), add(0x1c, ptr), 0x124, codesize(), callvalue())) { + let ptr_ := mload(0x40) + returndatacopy(ptr_, callvalue(), returndatasize()) + revert(ptr_, returndatasize()) + } + + break + } + } + + mstore(callvalue(), 0x01) + return(callvalue(), 0x20) + } + } + /// @inheritdoc ICrossChainReceiverFactory function call(address payable target, uint256 value, bytes calldata data) external diff --git a/src/interfaces/ICrossChainReceiverFactory.sol b/src/interfaces/ICrossChainReceiverFactory.sol index 3b14dcde3..94b596f93 100644 --- a/src/interfaces/ICrossChainReceiverFactory.sol +++ b/src/interfaces/ICrossChainReceiverFactory.sol @@ -21,6 +21,10 @@ interface ICrossChainReceiverFactory is IERC1271, IERC5267, IOwnable { /// Only available on proxies function approvePermit2(IERC20 token, uint256 amount) external returns (bool); + /// Utility function for getting stuck native/tokens out of the ERC2771-forwarding multicall contract + /// @dev This function DOES NOT WORK if the token implements ERC2771 with the multicall as its forwarder + function getFromMulticall(IERC20 token, address payable recipient) external returns (bool); + /// Only available on proxies function call(address payable target, uint256 value, bytes calldata data) external returns (bytes memory); From eec9c1f94d60b8fb9b20a07a9b72d3ba0c55a900 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Sat, 13 Dec 2025 08:28:58 +0100 Subject: [PATCH 56/72] Handle chains without a (wrapped) native asset --- src/CrossChainReceiverFactory.sol | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index d896d5ce0..f74ff5060 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -100,6 +100,8 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte ); IWrappedNative private immutable _WNATIVE = IWrappedNative(payable(address(uint160(uint256(bytes32(_WNATIVE_STORAGE.code)))))); + bool private immutable _HAS_WNATIVE = true; + bool private immutable _MISSING_WNATIVE = false; address private constant _PERMIT2_ADDRESS = 0x000000000022D473030F116dDEE9F6B43aC78BA3; ISignatureTransfer private constant _PERMIT2 = ISignatureTransfer(_PERMIT2_ADDRESS); @@ -139,8 +141,14 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte ); require(_CALL_TYPEHASH == keccak256("Call(address target,uint8 revertPolicy,uint256 value,bytes data)")); - // do some behavioral checks on `_WNATIVE` - { + if (address(_WNATIVE) == address(0)) { + require(_WNATIVE_STORAGE.codehash == 0xa4675c945174b9ec4e7010035cbc327beed918e1ea949cf630df20b201167a0c); + // `_WNATIVE` is deliberately unset + _HAS_WNATIVE = false; + _MISSING_WNATIVE = true; + } else { + // do some behavioral checks on `_WNATIVE` + // we need some value in order to perform the behavioral checks require(address(this).balance > 1 wei); @@ -337,6 +345,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte noDelegateCall returns (ICrossChainReceiverFactory proxy) { + setOwnerNotCleanup = setOwnerNotCleanup.or(_MISSING_WNATIVE); bytes32 proxyInitCode0 = _proxyInitCode0; bytes32 proxyInitCode1 = _proxyInitCode1; assembly ("memory-safe") { @@ -381,6 +390,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte /// @inheritdoc ICrossChainReceiverFactory function approvePermit2(IERC20 token, uint256 amount) external override onlyProxy returns (bool) { if (token == _NATIVE) { + require(!_MISSING_WNATIVE); token = _WNATIVE; assembly ("memory-safe") { if iszero(call(gas(), token, amount, codesize(), callvalue(), codesize(), callvalue())) { @@ -877,9 +887,9 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte unchecked { if (address(this).balance < value) { - IWrappedNative wnative = _WNATIVE; - uint256 wrappedBalance; + IWrappedNative wnative = _WNATIVE; + bool hasWnative = _HAS_WNATIVE; assembly ("memory-safe") { mstore(0x00, 0x70a08231) // `IERC20.balanceOf.selector` mstore(0x20, address()) @@ -889,7 +899,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte revert(codesize(), callvalue()) } - wrappedBalance := mload(callvalue()) + wrappedBalance := mul(_HAS_WNATIVE, mload(callvalue())) } uint256 toUnwrap = (address(this).balance + wrappedBalance < value).ternary( @@ -898,7 +908,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte value = toUnwrap + address(this).balance; assembly ("memory-safe") { - mstore(callvalue(), 0x2e1a7d4d) // IWrappedNative.withdraw.selector + mstore(callvalue(), 0x2e1a7d4d) // `IWrappedNative.withdraw.selector` mstore(0x20, toUnwrap) if iszero(call(gas(), wnative, callvalue(), 0x1c, 0x24, codesize(), callvalue())) { @@ -1141,7 +1151,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte } receive() external payable override onlyProxy { - if (msg.sender != address(_WNATIVE)) { + if ((msg.sender != address(_WNATIVE)).andNot(_MISSING_WNATIVE)) { IWrappedNative wnative = _WNATIVE; assembly ("memory-safe") { if iszero(call(gas(), wnative, callvalue(), codesize(), returndatasize(), codesize(), returndatasize())) From 85190466dbfeac5314c5f80d489e107ff83d8ddc Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Sat, 13 Dec 2025 08:36:42 +0100 Subject: [PATCH 57/72] `forge fmt` --- src/CrossChainReceiverFactory.sol | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index f74ff5060..e4db059f8 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -435,9 +435,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte revert(ptr_, returndatasize()) } let amount := mload(callvalue()) - if iszero(amount) { - break - } + if iszero(amount) { break } let ptr := mload(0x40) @@ -457,7 +455,9 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte mstore(add(0x120, ptr), 0x44) // calls[0].data.length - if iszero(call(gas(), MULTICALL_ADDRESS, callvalue(), add(0x1c, ptr), 0x168, codesize(), callvalue())) { + if iszero( + call(gas(), MULTICALL_ADDRESS, callvalue(), add(0x1c, ptr), 0x168, codesize(), callvalue()) + ) { let ptr_ := mload(0x40) returndatacopy(ptr_, callvalue(), returndatasize()) revert(ptr_, returndatasize()) @@ -468,9 +468,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte { let amount := balance(MULTICALL_ADDRESS) - if iszero(amount) { - break - } + if iszero(amount) { break } let ptr := mload(0x40) @@ -485,7 +483,9 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte mstore(add(0x100, ptr), 0x80) // calls[0].data.offset mstore(add(0x120, ptr), callvalue()) // calls[0].data.length - if iszero(call(gas(), MULTICALL_ADDRESS, callvalue(), add(0x1c, ptr), 0x124, codesize(), callvalue())) { + if iszero( + call(gas(), MULTICALL_ADDRESS, callvalue(), add(0x1c, ptr), 0x124, codesize(), callvalue()) + ) { let ptr_ := mload(0x40) returndatacopy(ptr_, callvalue(), returndatasize()) revert(ptr_, returndatasize()) From 464aecf4ecdfc49af0b338369d6fe3c929e20861 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Sat, 13 Dec 2025 09:07:42 +0100 Subject: [PATCH 58/72] DRY --- src/CrossChainReceiverFactory.sol | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index e4db059f8..a7a38e626 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -977,11 +977,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte } } } else { - _requireProxy(); - address msgSender = _msgSender(); - if (msgSender != address(this) && msgSender != super.owner()) { - _permissionDenied(); - } + _requireOwner(); } selfdestruct(beneficiary); } From 3a2350ff7b89693c8764a0c701266331ab7ef354 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Sat, 13 Dec 2025 09:29:58 +0100 Subject: [PATCH 59/72] Bug! Missing padding --- src/CrossChainReceiverFactory.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index a7a38e626..02bcd3894 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -451,7 +451,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte mstore(add(0x164, ptr), amount) mstore(add(0x144, ptr), recipient) - mstore(add(0x130, ptr), 0xa9059cbb) // `IERC20.transfer.selector` with `recipient`'s padding + mstore(add(0x130, ptr), 0xa9059cbb000000000000000000000000) // `IERC20.transfer.selector` with `recipient`'s padding mstore(add(0x120, ptr), 0x44) // calls[0].data.length From 0d5bc01d6ee29cbda697e970f36007c35d042e0b Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Sat, 13 Dec 2025 12:05:32 +0100 Subject: [PATCH 60/72] Bug! Wrong bounds on revert region --- src/CrossChainReceiverFactory.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index 02bcd3894..a38a08ec9 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -1153,7 +1153,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte if iszero(call(gas(), wnative, callvalue(), codesize(), returndatasize(), codesize(), returndatasize())) { // this should never happen - revert(codesize(), callvalue()) + revert(codesize(), 0x00) } } } From 0ee5a584397040f5fcc66fe20ace8374c4f2fb6a Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Tue, 16 Dec 2025 12:44:20 +0100 Subject: [PATCH 61/72] Compilation error --- src/CrossChainReceiverFactory.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index a38a08ec9..ebdebc3e9 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -899,7 +899,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte revert(codesize(), callvalue()) } - wrappedBalance := mul(_HAS_WNATIVE, mload(callvalue())) + wrappedBalance := mul(hasWnative, mload(callvalue())) } uint256 toUnwrap = (address(this).balance + wrappedBalance < value).ternary( From cd9d20ec2c91fef5de582f48e763a4c9a77dd877 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Tue, 16 Dec 2025 12:44:37 +0100 Subject: [PATCH 62/72] Golf --- src/CrossChainReceiverFactory.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index ebdebc3e9..159699a08 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -1153,7 +1153,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte if iszero(call(gas(), wnative, callvalue(), codesize(), returndatasize(), codesize(), returndatasize())) { // this should never happen - revert(codesize(), 0x00) + revert(codesize(), calldatasize()) } } } From 04cb56db32b79e0db9fe04827bd6c48373c58a8c Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Tue, 16 Dec 2025 13:06:40 +0100 Subject: [PATCH 63/72] Bug! No revert on short returndata from `balanceOf` inside `getFromMulticall` --- src/CrossChainReceiverFactory.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index 159699a08..f7989e74f 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -434,6 +434,8 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte returndatacopy(ptr_, callvalue(), returndatasize()) revert(ptr_, returndatasize()) } + if gt(0x20, returndatasize()) { revert(codesize(), callvalue()) } + let amount := mload(callvalue()) if iszero(amount) { break } From f6d03001e1121029d32087fb24ebf283c4234b90 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Mon, 22 Dec 2025 13:52:19 +0100 Subject: [PATCH 64/72] WIP: add a deploy-time check that the hardcoded EIP-150 rule (in ERC2771-forwarding `MultiCall`) is implemented correctly --- src/CrossChainReceiverFactory.sol | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index f7989e74f..be4e6719b 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -112,6 +112,22 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte error InvalidSigner(); error SignatureExpired(uint256 deadline); + function _eip150RatioTest() private returns (bool isWeird) { + address invalidTarget; + assembly ("memory-safe") { + mstore(0x00, 0x6001600060fe8153f3) + invalidTarget := create(0x00, 0x17, 0x09) + if iszero(invalidTarget) { revert(0x00, 0x00) } + } + + Call[] memory calls = new Call[](1); + calls[0].target = invalidTarget; + calls[0].revertPolicy = IMultiCall.RevertPolicy.CONTINUE; + + bytes memory data = abi.encodeCall(IMultiCall.multicall, (calls, 0)); + (isWeird,) = MULTICALL_ADDRESS.call{gas: 100_000}(data); + } + constructor() payable { // This bit of bizarre functionality is required to accommodate Foundry's `deployCodeTo` // cheat code. It is a no-op at deploy time. From 64503b69150b6d8c34956c50224500cf30f78f6a Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Mon, 22 Dec 2025 17:10:21 +0100 Subject: [PATCH 65/72] Do a deploy-time test for EIP150 (approximately) and conditionally retrieve an alternative `MultiCall` address if it fails --- src/CrossChainReceiverFactory.sol | 107 ++++++++++++++++++----------- src/multicall/MultiCallContext.sol | 28 ++++++-- 2 files changed, 86 insertions(+), 49 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index be4e6719b..5f800461a 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -11,7 +11,7 @@ import {ISignatureTransfer} from "@permit2/interfaces/ISignatureTransfer.sol"; import {ICrossChainReceiverFactory} from "./interfaces/ICrossChainReceiverFactory.sol"; import {AbstractOwnable, OwnableImpl, TwoStepOwnable} from "./utils/TwoStepOwnable.sol"; -import {IMultiCall, MultiCallContext, MULTICALL_ADDRESS} from "./multicall/MultiCallContext.sol"; +import {IMultiCall, MultiCallContext, EIP150_MULTICALL_ADDRESS} from "./multicall/MultiCallContext.sol"; import {FastLogic} from "./utils/FastLogic.sol"; import {Ternary} from "./utils/Ternary.sol"; @@ -68,49 +68,43 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte hex"1815601657fe5b7f60143603803560601c6d", uint112(uint160(_WNATIVE_SETTER)), hex"14336c", - uint40(uint104(uint160(MULTICALL_ADDRESS)) >> 64), + uint40(uint104(uint160(EIP150_MULTICALL_ADDRESS)) >> 64), hex"3d527f", - uint64(uint104(uint160(MULTICALL_ADDRESS))), + uint64(uint104(uint160(EIP150_MULTICALL_ADDRESS))), hex"1416602e57fe5b3d54604b57583d55803d3d373d34f03d8159526d6045573dfd5b5260203df35b30ff60901b5952604e3df3" ) ); bytes32 private constant _WNATIVE_STORAGE_SALT = keccak256("Wrapped Native Token Address"); - address private constant _WNATIVE_STORAGE = address( - uint160( - uint256( - keccak256( - abi.encodePacked( - hex"d694", - address( - uint160( - uint256( - keccak256( - abi.encodePacked( - hex"ff", _TOEHOLD, _WNATIVE_STORAGE_SALT, _WNATIVE_STORAGE_INITHASH - ) + + function _getImmutableStorageAddress(bytes32 salt) private view returns (address) { + return address( + uint160( + uint256( + keccak256( + abi.encodePacked( + hex"d694", + address( + uint160( + uint256( + keccak256(abi.encodePacked(hex"ff", _TOEHOLD, salt, _WNATIVE_STORAGE_INITHASH)) ) ) - ) - ), - hex"01" + ), + hex"01" + ) ) ) ) - ) - ); - IWrappedNative private immutable _WNATIVE = - IWrappedNative(payable(address(uint160(uint256(bytes32(_WNATIVE_STORAGE.code)))))); - bool private immutable _HAS_WNATIVE = true; - bool private immutable _MISSING_WNATIVE = false; + ); + } - address private constant _PERMIT2_ADDRESS = 0x000000000022D473030F116dDEE9F6B43aC78BA3; - ISignatureTransfer private constant _PERMIT2 = ISignatureTransfer(_PERMIT2_ADDRESS); + function _getImmutableAddress(bytes32 salt) private view returns (address) { + return address(uint160(uint256(bytes32(_getImmutableStorageAddress(salt).code)))); + } - error DeploymentFailed(); - error ApproveFailed(); - error InvalidNonce(); - error InvalidSigner(); - error SignatureExpired(uint256 deadline); + IWrappedNative private immutable _WNATIVE = IWrappedNative(payable(_getImmutableAddress(_WNATIVE_STORAGE_SALT))); + bool private immutable _HAS_WNATIVE = true; + bool private immutable _MISSING_WNATIVE = false; function _eip150RatioTest() private returns (bool isWeird) { address invalidTarget; @@ -120,14 +114,27 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte if iszero(invalidTarget) { revert(0x00, 0x00) } } - Call[] memory calls = new Call[](1); + IMultiCall.Call[] memory calls = new IMultiCall.Call[](1); calls[0].target = invalidTarget; calls[0].revertPolicy = IMultiCall.RevertPolicy.CONTINUE; bytes memory data = abi.encodeCall(IMultiCall.multicall, (calls, 0)); - (isWeird,) = MULTICALL_ADDRESS.call{gas: 100_000}(data); + (isWeird,) = EIP150_MULTICALL_ADDRESS.call{gas: 100_000}(data); } + bytes32 private constant _MULTICALL_STORAGE_SALT = keccak256("ERC2771-forwarding MultiCall Address"); + IMultiCall private immutable _CHAIN_SPECIFIC_MULTICALL = + IMultiCall(payable(_eip150RatioTest() ? _getImmutableAddress(_MULTICALL_STORAGE_SALT) : EIP150_MULTICALL_ADDRESS)); + + address private constant _PERMIT2_ADDRESS = 0x000000000022D473030F116dDEE9F6B43aC78BA3; + ISignatureTransfer private constant _PERMIT2 = ISignatureTransfer(_PERMIT2_ADDRESS); + + error DeploymentFailed(); + error ApproveFailed(); + error InvalidNonce(); + error InvalidSigner(); + error SignatureExpired(uint256 deadline); + constructor() payable { // This bit of bizarre functionality is required to accommodate Foundry's `deployCodeTo` // cheat code. It is a no-op at deploy time. @@ -157,8 +164,18 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte ); require(_CALL_TYPEHASH == keccak256("Call(address target,uint8 revertPolicy,uint256 value,bytes data)")); + { + IMultiCall.Call[] memory calls = new IMultiCall.Call[](1); + calls[0].target = address(4); // identity + calls[0].data = "Hello, World!"; + IMultiCall.Result[] memory results = _MULTICALL().multicall(calls, 1); + require(results.length == 1); + require(results[0].success); + require(keccak256(results[0].data) == keccak256("Hello, World!")); + } + if (address(_WNATIVE) == address(0)) { - require(_WNATIVE_STORAGE.codehash == 0xa4675c945174b9ec4e7010035cbc327beed918e1ea949cf630df20b201167a0c); + require(_getImmutableStorageAddress(_WNATIVE_STORAGE_SALT).codehash == 0xa4675c945174b9ec4e7010035cbc327beed918e1ea949cf630df20b201167a0c); // `_WNATIVE` is deliberately unset _HAS_WNATIVE = false; _MISSING_WNATIVE = true; @@ -243,6 +260,10 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte return super.pendingOwner(); } + function _MULTICALL() internal view override returns (IMultiCall) { + return _CHAIN_SPECIFIC_MULTICALL; + } + /// @inheritdoc IERC1271 function isValidSignature(bytes32 hash, bytes calldata signature) external @@ -440,11 +461,12 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte /// @inheritdoc ICrossChainReceiverFactory function getFromMulticall(IERC20 token, address payable recipient) external override returns (bool) { + IMultiCall MULTICALL = _MULTICALL(); assembly ("memory-safe") { for {} true {} { if shl(0x60, xor(_NATIVE_ADDRESS, token)) { mstore(callvalue(), 0x70a08231) - mstore(0x20, MULTICALL_ADDRESS) + mstore(0x20, MULTICALL) if iszero(staticcall(gas(), token, 0x1c, 0x24, callvalue(), 0x20)) { let ptr_ := mload(0x40) returndatacopy(ptr_, callvalue(), returndatasize()) @@ -474,7 +496,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte mstore(add(0x120, ptr), 0x44) // calls[0].data.length if iszero( - call(gas(), MULTICALL_ADDRESS, callvalue(), add(0x1c, ptr), 0x168, codesize(), callvalue()) + call(gas(), MULTICALL, callvalue(), add(0x1c, ptr), 0x168, codesize(), callvalue()) ) { let ptr_ := mload(0x40) returndatacopy(ptr_, callvalue(), returndatasize()) @@ -485,7 +507,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte } { - let amount := balance(MULTICALL_ADDRESS) + let amount := balance(MULTICALL) if iszero(amount) { break } let ptr := mload(0x40) @@ -502,7 +524,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte mstore(add(0x120, ptr), callvalue()) // calls[0].data.length if iszero( - call(gas(), MULTICALL_ADDRESS, callvalue(), add(0x1c, ptr), 0x124, codesize(), callvalue()) + call(gas(), MULTICALL, callvalue(), add(0x1c, ptr), 0x124, codesize(), callvalue()) ) { let ptr_ := mload(0x40) returndatacopy(ptr_, callvalue(), returndatasize()) @@ -937,12 +959,13 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte } } + IMultiCall MULTICALL = _MULTICALL(); assembly ("memory-safe") { let dataLength := mload(data) mstore(data, 0x669a7d5e) // `IMultiCall.multicall.selector` // we won't bother to restore `data.length` because this block never returns to Solidity - let success := call(gas(), MULTICALL_ADDRESS, value, add(0x1c, data), dataLength, codesize(), callvalue()) + let success := call(gas(), MULTICALL, value, add(0x1c, data), dataLength, codesize(), callvalue()) // technically, this is not memory safe because there could be a hidden // compiler-allocated object at the end of `data` and the returndata from the `CALL` @@ -954,7 +977,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte let rds := returndatasize() - let multicallBalance := balance(MULTICALL_ADDRESS) + let multicallBalance := balance(MULTICALL) if multicallBalance { // get any excess native value back out of `MultiCall` @@ -970,7 +993,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte mstore(add(0x100, ptr), 0x80) // calls[0].data.offset mstore(add(0x120, ptr), callvalue()) // calls[0].data.length - if iszero(call(gas(), MULTICALL_ADDRESS, callvalue(), add(0x1c, ptr), 0x124, codesize(), callvalue())) { + if iszero(call(gas(), MULTICALL, callvalue(), add(0x1c, ptr), 0x124, codesize(), callvalue())) { // this should never happen revert(codesize(), callvalue()) } diff --git a/src/multicall/MultiCallContext.sol b/src/multicall/MultiCallContext.sol index 1b97a7c21..87d2b7fd2 100644 --- a/src/multicall/MultiCallContext.sol +++ b/src/multicall/MultiCallContext.sol @@ -29,27 +29,37 @@ interface IMultiCall { receive() external payable; } -address constant MULTICALL_ADDRESS = 0x00000000000000CF9E3c5A26621af382fA17f24f; +address constant EIP150_MULTICALL_ADDRESS = 0x00000000000000CF9E3c5A26621af382fA17f24f; abstract contract MultiCallContext is Context { using FastLogic for bool; - IMultiCall internal constant _MULTICALL = IMultiCall(payable(MULTICALL_ADDRESS)); + function _MULTICALL() internal view virtual returns (IMultiCall) { + return IMultiCall(payable(EIP150_MULTICALL_ADDRESS)); + } + + function _isForwarded(address multicall) internal view returns (bool) { + return super._isForwarded().or(super._msgSender() == address(multicall)); + } function _isForwarded() internal view virtual override returns (bool) { - return super._isForwarded().or(super._msgSender() == address(_MULTICALL)); + return MultiCallContext._isForwarded(address(_MULTICALL())); } - function _msgData() internal view virtual override returns (bytes calldata r) { + function _msgData(address multicall) internal view returns (bytes calldata r) { address sender = super._msgSender(); r = super._msgData(); assembly ("memory-safe") { r.length := - sub(r.length, mul(0x14, eq(MULTICALL_ADDRESS, and(0xffffffffffffffffffffffffffffffffffffffff, sender)))) + sub(r.length, mul(0x14, lt(0x00, shl(0x60, xor(multicall, sender))))) } } - function _msgSender() internal view virtual override returns (address sender) { + function _msgData() internal view virtual override returns (bytes calldata) { + return MultiCallContext._msgData(address(_MULTICALL())); + } + + function _msgSender(address multicall) internal view returns (address sender) { sender = super._msgSender(); bytes calldata data = super._msgData(); assembly ("memory-safe") { @@ -60,9 +70,13 @@ abstract contract MultiCallContext is Context { sender, mul( xor(shr(0x60, calldataload(add(data.offset, sub(data.length, 0x14)))), sender), - and(lt(0x03, data.length), iszero(shl(0x60, xor(MULTICALL_ADDRESS, sender)))) + and(lt(0x03, data.length), iszero(shl(0x60, xor(multicall, sender)))) ) ) } } + + function _msgSender() internal view virtual override returns (address) { + return MultiCallContext._msgSender(address(_MULTICALL())); + } } From 40794326c0d8dc1ead623cccc70175c225903946 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Mon, 22 Dec 2025 17:29:11 +0100 Subject: [PATCH 66/72] Golf --- src/CrossChainReceiverFactory.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index 5f800461a..144d8547c 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -109,8 +109,8 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte function _eip150RatioTest() private returns (bool isWeird) { address invalidTarget; assembly ("memory-safe") { - mstore(0x00, 0x6001600060fe8153f3) - invalidTarget := create(0x00, 0x17, 0x09) + mstore(0x00, 0x5b5860fe3d533df3) + invalidTarget := create(0x00, 0x18, 0x08) if iszero(invalidTarget) { revert(0x00, 0x00) } } From 85423884668829ebbc12bf518658fcf2e04f023c Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Mon, 22 Dec 2025 17:37:39 +0100 Subject: [PATCH 67/72] Bug! Wrong echo result when calling through the _ERC2771 forwarding_ multicall --- src/CrossChainReceiverFactory.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index 144d8547c..6b1487a59 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -171,7 +171,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte IMultiCall.Result[] memory results = _MULTICALL().multicall(calls, 1); require(results.length == 1); require(results[0].success); - require(keccak256(results[0].data) == keccak256("Hello, World!")); + require(keccak256(results[0].data) == keccak256(bytes.concat("Hello, World!", bytes20(uint160(address(this)))))); } if (address(_WNATIVE) == address(0)) { From f27783297dbae179d6fcaf3b53a13a42e3fc9c97 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Mon, 22 Dec 2025 17:47:03 +0100 Subject: [PATCH 68/72] Move `deployCodeTo` shim into `_eip150RatioTest` where it was causing problems --- src/CrossChainReceiverFactory.sol | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index 6b1487a59..e18a54d9d 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -111,7 +111,17 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte assembly ("memory-safe") { mstore(0x00, 0x5b5860fe3d533df3) invalidTarget := create(0x00, 0x18, 0x08) - if iszero(invalidTarget) { revert(0x00, 0x00) } + } + + if (invalidTarget == address(0)) { + // This bit of bizarre functionality is required to accommodate Foundry's `deployCodeTo` + // cheat code. It is a no-op at deploy time. + if ((block.chainid == 31337).and(msg.sender == address(_WNATIVE)).and(msg.value > 1 wei)) { + assembly ("memory-safe") { + stop() + } + } + revert(); } IMultiCall.Call[] memory calls = new IMultiCall.Call[](1); @@ -136,14 +146,6 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte error SignatureExpired(uint256 deadline); constructor() payable { - // This bit of bizarre functionality is required to accommodate Foundry's `deployCodeTo` - // cheat code. It is a no-op at deploy time. - if ((block.chainid == 31337).and(msg.sender == address(_WNATIVE)).and(msg.value > 1 wei)) { - assembly ("memory-safe") { - stop() - } - } - require(((msg.sender == _TOEHOLD).and(uint160(address(this)) >> 104 == 0)).or(block.chainid == 31337)); require(uint160(_WNATIVE_SETTER) >> 112 == 0); require(_NAMEHASH == keccak256(bytes(name))); From c7b0bdb764b52bab9c2a6f36a1e1c1dab0a893d0 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Mon, 22 Dec 2025 18:00:56 +0100 Subject: [PATCH 69/72] Bug! Move `deployCodeTo` kludge yet again --- src/CrossChainReceiverFactory.sol | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index e18a54d9d..68134d4ac 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -107,21 +107,19 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte bool private immutable _MISSING_WNATIVE = false; function _eip150RatioTest() private returns (bool isWeird) { + // This bit of bizarre functionality is required to accommodate Foundry's `deployCodeTo` + // cheat code. It is a no-op at deploy time. + if ((block.chainid == 31337).and(msg.sender == address(_WNATIVE)).and(msg.value > 1 wei)) { + assembly ("memory-safe") { + stop() + } + } + address invalidTarget; assembly ("memory-safe") { mstore(0x00, 0x5b5860fe3d533df3) invalidTarget := create(0x00, 0x18, 0x08) - } - - if (invalidTarget == address(0)) { - // This bit of bizarre functionality is required to accommodate Foundry's `deployCodeTo` - // cheat code. It is a no-op at deploy time. - if ((block.chainid == 31337).and(msg.sender == address(_WNATIVE)).and(msg.value > 1 wei)) { - assembly ("memory-safe") { - stop() - } - } - revert(); + if iszero(invalidTarget) { revert(codesize(), 0x00) } } IMultiCall.Call[] memory calls = new IMultiCall.Call[](1); From d04aa324c63b2f085a169ded89bb7a44411d4f60 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Mon, 29 Dec 2025 20:14:06 +0100 Subject: [PATCH 70/72] Treat `_CHAIN_SPECIFIC_MULTICALL` the same as `_WNATIVE` when constructing `CrossChainReceiverFactory`. Namely, it is a mandatory "argument" --- src/CrossChainReceiverFactory.sol | 66 +++++++++++++++++++------------ 1 file changed, 40 insertions(+), 26 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index 68134d4ac..912c412fa 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -106,33 +106,8 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte bool private immutable _HAS_WNATIVE = true; bool private immutable _MISSING_WNATIVE = false; - function _eip150RatioTest() private returns (bool isWeird) { - // This bit of bizarre functionality is required to accommodate Foundry's `deployCodeTo` - // cheat code. It is a no-op at deploy time. - if ((block.chainid == 31337).and(msg.sender == address(_WNATIVE)).and(msg.value > 1 wei)) { - assembly ("memory-safe") { - stop() - } - } - - address invalidTarget; - assembly ("memory-safe") { - mstore(0x00, 0x5b5860fe3d533df3) - invalidTarget := create(0x00, 0x18, 0x08) - if iszero(invalidTarget) { revert(codesize(), 0x00) } - } - - IMultiCall.Call[] memory calls = new IMultiCall.Call[](1); - calls[0].target = invalidTarget; - calls[0].revertPolicy = IMultiCall.RevertPolicy.CONTINUE; - - bytes memory data = abi.encodeCall(IMultiCall.multicall, (calls, 0)); - (isWeird,) = EIP150_MULTICALL_ADDRESS.call{gas: 100_000}(data); - } - bytes32 private constant _MULTICALL_STORAGE_SALT = keccak256("ERC2771-forwarding MultiCall Address"); - IMultiCall private immutable _CHAIN_SPECIFIC_MULTICALL = - IMultiCall(payable(_eip150RatioTest() ? _getImmutableAddress(_MULTICALL_STORAGE_SALT) : EIP150_MULTICALL_ADDRESS)); + IMultiCall private immutable _CHAIN_SPECIFIC_MULTICALL = IMultiCall(payable(_getImmutableAddress(_MULTICALL_STORAGE_SALT))); address private constant _PERMIT2_ADDRESS = 0x000000000022D473030F116dDEE9F6B43aC78BA3; ISignatureTransfer private constant _PERMIT2 = ISignatureTransfer(_PERMIT2_ADDRESS); @@ -144,6 +119,14 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte error SignatureExpired(uint256 deadline); constructor() payable { + // This bit of bizarre functionality is required to accommodate Foundry's `deployCodeTo` + // cheat code. It is a no-op at deploy time. + if ((block.chainid == 31337).and(msg.sender == address(_WNATIVE)).and(msg.value > 1 wei)) { + assembly ("memory-safe") { + stop() + } + } + require(((msg.sender == _TOEHOLD).and(uint160(address(this)) >> 104 == 0)).or(block.chainid == 31337)); require(uint160(_WNATIVE_SETTER) >> 112 == 0); require(_NAMEHASH == keccak256(bytes(name))); @@ -165,8 +148,39 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte require(_CALL_TYPEHASH == keccak256("Call(address target,uint8 revertPolicy,uint256 value,bytes data)")); { + // Check that an OOG revert is bubbled, even when `revertPolicy == CONTINUE` + address invalidTarget; + assembly ("memory-safe") { + mstore(0x00, 0x5b5860fe3d533df3) + invalidTarget := create(0x00, 0x18, 0x08) + if iszero(invalidTarget) { revert(codesize(), 0x00) } + } + IMultiCall.Call[] memory calls = new IMultiCall.Call[](1); + calls[0].target = invalidTarget; + calls[0].revertPolicy = IMultiCall.RevertPolicy.CONTINUE; + bytes memory data = abi.encodeCall(IMultiCall.multicall, (calls, 0)); + (bool success,) = address(_MULTICALL()).call{gas: 100_000}(data); + require(!success); + + // Check that a non-OOG revert is swallowed when `revertPolicy == CONTINUE` + address revertTarget; + assembly ("memory-safe") { + mstore(0x00, 0x623d3dfd3d526003601df3) + revertTarget := create(0x00, 0x15, 0x0b) + if iszero(revertTarget) { revert(codesize(), 0x00) } + } + + calls[0].target = revertTarget; + IMultiCall.Result[] memory results = _MULTICALL().multicall{gas: 100_000}(calls, 1); + require(results.length == 1); + require(!results[0].success); + require(results[0].data.length == 0); + + // Check that calling the identity precompile returns success and the expected echoed + // data (including appended ERC2771 metadata) calls[0].target = address(4); // identity + calls[0].revertPolicy = IMultiCall.RevertPolicy.REVERT; calls[0].data = "Hello, World!"; IMultiCall.Result[] memory results = _MULTICALL().multicall(calls, 1); require(results.length == 1); From b6800bb1e9a6a4a11142e69ad7128936db2ca055 Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Tue, 30 Dec 2025 10:15:42 +0100 Subject: [PATCH 71/72] Substitute `_ADDRESS_THIS_SENTINEL` as `recipient` for `address(this)` in `getFromMulticall` --- src/CrossChainReceiverFactory.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index f7989e74f..606d65296 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -425,6 +425,7 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte /// @inheritdoc ICrossChainReceiverFactory function getFromMulticall(IERC20 token, address payable recipient) external override returns (bool) { assembly ("memory-safe") { + recipient := xor(recipient, mul(iszero(shl(0x60, xor(_ADDRESS_THIS_SENTINEL, recipient))), xor(address(), recipient))) for {} true {} { if shl(0x60, xor(_NATIVE_ADDRESS, token)) { mstore(callvalue(), 0x70a08231) From 035073ec2dd87eb388d21184d0421e14ea4c64aa Mon Sep 17 00:00:00 2001 From: Duncan Townsend Date: Tue, 30 Dec 2025 10:16:46 +0100 Subject: [PATCH 72/72] `forge fmt` --- src/CrossChainReceiverFactory.sol | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index 606d65296..924c371fd 100644 --- a/src/CrossChainReceiverFactory.sol +++ b/src/CrossChainReceiverFactory.sol @@ -129,7 +129,9 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte _SENTINEL_DOMAIN_SEPARATOR == keccak256( abi.encode( - keccak256("EIP712Domain(string name,address verifyingContract)"), _NAMEHASH, _ADDRESS_THIS_SENTINEL + keccak256("EIP712Domain(string name,address verifyingContract)"), + _NAMEHASH, + _ADDRESS_THIS_SENTINEL ) ) ); @@ -288,16 +290,14 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte proof.length := calldataload(proof.offset) proof.offset := add(0x20, proof.offset) } - return _verifyDeploymentRootHash(_getMerkleRoot(proof, _hashLeaf(hash)), originalOwner).ternary( - IERC1271.isValidSignature.selector, bytes4(0xffffffff) - ); + return _verifyDeploymentRootHash(_getMerkleRoot(proof, _hashLeaf(hash)), originalOwner) + .ternary(IERC1271.isValidSignature.selector, bytes4(0xffffffff)); } } // ERC7739 validation - return _verifyERC7739NestedTypedSignature(hash, signature, super.owner()).ternary( - IERC1271.isValidSignature.selector, bytes4(0xffffffff) - ); + return _verifyERC7739NestedTypedSignature(hash, signature, super.owner()) + .ternary(IERC1271.isValidSignature.selector, bytes4(0xffffffff)); } function _hashLeaf(bytes32 signingHash) private view returns (bytes32 leafHash) { @@ -425,7 +425,10 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte /// @inheritdoc ICrossChainReceiverFactory function getFromMulticall(IERC20 token, address payable recipient) external override returns (bool) { assembly ("memory-safe") { - recipient := xor(recipient, mul(iszero(shl(0x60, xor(_ADDRESS_THIS_SENTINEL, recipient))), xor(address(), recipient))) + recipient := xor( + recipient, + mul(iszero(shl(0x60, xor(_ADDRESS_THIS_SENTINEL, recipient))), xor(address(), recipient)) + ) for {} true {} { if shl(0x60, xor(_NATIVE_ADDRESS, token)) { mstore(callvalue(), 0x70a08231) @@ -905,9 +908,8 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte wrappedBalance := mul(hasWnative, mload(callvalue())) } - uint256 toUnwrap = (address(this).balance + wrappedBalance < value).ternary( - wrappedBalance, value - address(this).balance - ); + uint256 toUnwrap = (address(this).balance + wrappedBalance < value) + .ternary(wrappedBalance, value - address(this).balance); value = toUnwrap + address(this).balance; assembly ("memory-safe") { @@ -1153,8 +1155,9 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte if ((msg.sender != address(_WNATIVE)).andNot(_MISSING_WNATIVE)) { IWrappedNative wnative = _WNATIVE; assembly ("memory-safe") { - if iszero(call(gas(), wnative, callvalue(), codesize(), returndatasize(), codesize(), returndatasize())) - { + if iszero( + call(gas(), wnative, callvalue(), codesize(), returndatasize(), codesize(), returndatasize()) + ) { // this should never happen revert(codesize(), calldatasize()) }