diff --git a/src/CrossChainReceiverFactory.sol b/src/CrossChainReceiverFactory.sol index cb03b2821..67ebe505a 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 {AbstractOwnable, OwnableImpl, TwoStepOwnable} from "./utils/TwoStepOwnable.sol"; +import {IMultiCall, MultiCallContext, EIP150_MULTICALL_ADDRESS} from "./multicall/MultiCallContext.sol"; import {FastLogic} from "./utils/FastLogic.sol"; import {Ternary} from "./utils/Ternary.sol"; @@ -31,6 +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(uint256(0x5af43d3d93803e602357fd5bf3 << 144)); bytes32 private immutable _proxyInitHash = keccak256( bytes.concat( hex"60253d8160093d39f33d3d3d3d363d3d37363d6c", @@ -38,9 +44,21 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte hex"5af43d3d93803e602357fd5bf3" ) ); + string public constant override name = "ZeroExCrossChainReceiver"; bytes32 private constant _NAMEHASH = 0x819c7f86c24229cd5fed5a41696eb0cd8b3f84cc632df73cfd985e8b100980e8; - IERC20 private constant _NATIVE = IERC20(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE); + 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; + + 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( @@ -50,41 +68,55 @@ 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)))))); + ); + } + + function _getImmutableAddress(bytes32 salt) private view returns (address) { + return address(uint160(uint256(bytes32(_getImmutableStorageAddress(salt).code)))); + } + + IWrappedNative private immutable _WNATIVE = IWrappedNative(payable(_getImmutableAddress(_WNATIVE_STORAGE_SALT))); + bool private immutable _HAS_WNATIVE = true; + bool private immutable _MISSING_WNATIVE = false; + + bytes32 private constant _MULTICALL_STORAGE_SALT = keccak256("ERC2771-forwarding 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); 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` @@ -98,9 +130,74 @@ 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( + _SENTINEL_DOMAIN_SEPARATOR + == keccak256( + abi.encode( + keccak256("EIP712Domain(string name,address verifyingContract)"), + _NAMEHASH, + _ADDRESS_THIS_SENTINEL + ) + ) + ); + require( + _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)")); - // do some behavioral checks on `_WNATIVE` { + // 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); + require(results[0].success); + require(keccak256(results[0].data) == keccak256(bytes.concat("Hello, World!", bytes20(uint160(address(this)))))); + } + + if (address(_WNATIVE) == address(0)) { + require(_getImmutableStorageAddress(_WNATIVE_STORAGE_SALT).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); @@ -147,6 +244,13 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte _; } + function _requireOwner() internal view override(AbstractOwnable, OwnableImpl) onlyProxy { + address msgSender = _msgSender(); + if (msgSender != address(this) && msgSender != super.owner()) { + _permissionDenied(); + } + } + /// @inheritdoc IERC165 function supportsInterface(bytes4 interfaceId) public @@ -159,8 +263,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(); } @@ -171,6 +276,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 @@ -222,20 +331,6 @@ 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 @@ -246,17 +341,30 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte proof.length := calldataload(proof.offset) proof.offset := add(0x20, proof.offset) } - - return _verifyDeploymentRootHash(_getMerkleRoot(proof, 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) { + 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(callvalue(), signingHash) + mstore(0x20, chainid()) + leafHash := keccak256(callvalue(), 0x40) + } } /// @inheritdoc IERC5267 @@ -288,19 +396,21 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte noDelegateCall returns (ICrossChainReceiverFactory proxy) { + setOwnerNotCleanup = setOwnerNotCleanup.or(_MISSING_WNATIVE); + bytes32 proxyInitCode0 = _proxyInitCode0; + bytes32 proxyInitCode1 = _proxyInitCode1; 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(0x1a, 0x5af43d3d93803e602357fd5bf3) - mstore(0x0d, address()) - mstore(returndatasize(), 0x60253d8160093d39f33d3d3d3d363d3d37363d6c) - proxy := create2(returndatasize(), 0x0c, 0x2e, salt) + mstore(callvalue(), proxyInitCode0) + mstore(0x20, proxyInitCode1) + proxy := create2(callvalue(), callvalue(), 0x2e, salt) if iszero(proxy) { - mstore(returndatasize(), 0x30116425) // selector for `DeploymentFailed()`. + mstore(callvalue(), 0x30116425) // selector for `DeploymentFailed()`. revert(0x1c, 0x04) } @@ -314,28 +424,29 @@ 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()) } } } /// @inheritdoc ICrossChainReceiverFactory - function setOwner(address owner) external override onlyFactory { - _setOwner(owner); + function setOwner(address owner_) external override onlyFactory { + _setOwner(owner_); } /// @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(), 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()) } } @@ -343,22 +454,107 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte assembly ("memory-safe") { let ptr := mload(0x40) - mstore(returndatasize(), 0x095ea7b3) // selector for `approve(address,uint256)` - mstore(0x20, 0x000000000022D473030F116dDEE9F6B43aC78BA3) // Permit2 + 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) + } + } + + /// @inheritdoc ICrossChainReceiverFactory + function getFromMulticall(IERC20 token, address payable recipient) external override returns (bool) { + IMultiCall MULTICALL = _MULTICALL(); + 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) + mstore(0x20, MULTICALL) + if iszero(staticcall(gas(), token, 0x1c, 0x24, callvalue(), 0x20)) { + let ptr_ := mload(0x40) + returndatacopy(ptr_, callvalue(), returndatasize()) + revert(ptr_, returndatasize()) + } + if gt(0x20, returndatasize()) { revert(codesize(), callvalue()) } + + 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), 0xa9059cbb000000000000000000000000) // `IERC20.transfer.selector` with `recipient`'s padding + + mstore(add(0x120, ptr), 0x44) // calls[0].data.length + + if iszero( + call(gas(), MULTICALL, callvalue(), add(0x1c, ptr), 0x168, codesize(), callvalue()) + ) { + let ptr_ := mload(0x40) + returndatacopy(ptr_, callvalue(), returndatasize()) + revert(ptr_, returndatasize()) + } + + break + } + + { + let amount := balance(MULTICALL) + 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, 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) } } @@ -373,15 +569,454 @@ 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(codesize(), callvalue()) } + } + + let paddedLength := and(not(0x1f), add(0x1f, returndatasize())) + mstore(add(add(0x20, ptr), paddedLength), callvalue()) + returndatacopy(add(0x40, ptr), callvalue(), returndatasize()) + + if iszero(success) { revert(add(0x40, mload(0x40)), returndatasize()) } + + mstore(add(0x20, ptr), returndatasize()) + mstore(ptr, 0x20) + return(ptr, add(0x40, paddedLength)) + } + } + + /// @inheritdoc ICrossChainReceiverFactory + function call(address payable target, IERC20 token, uint256 ppm, uint256 patchOffset, bytes calldata data) + external + payable + override + onlyOwner + returns (bytes memory) + { + assembly ("memory-safe") { + // empty data with offset == 0 is OK. otherwise, perform bounds checking + if iszero(lt(add(0x1f, patchOffset), data.length)) { + 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) + } + } + + let patchBytes + let value + for {} true {} { + 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)) { + 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 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) + } + + patchBytes := div(patchBytes, 1000000) + value := callvalue() + + break + } + + patchBytes := mul(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) + } + + patchBytes := div(patchBytes, 1000000) + value := patchBytes + break + } + + let ptr := mload(0x40) + calldatacopy(ptr, data.offset, data.length) + mstore(add(patchOffset, ptr), patchBytes) + + let success := call(gas(), target, value, ptr, data.length, codesize(), 0x00) + + // 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) } + } + + 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, ptr), returndatasize()) } + if iszero(success) { revert(add(0x40, mload(0x40)), returndatasize()) } mstore(add(0x20, ptr), returndatasize()) mstore(ptr, 0x20) - return(ptr, add(0x40, returndatasize())) + return(ptr, add(0x40, paddedLength)) + } + } + + function _useUnorderedNonce(uint256 nonce) private { + assembly ("memory-safe") { + let ptr := mload(0x40) + let wordPos := shr(0x08, nonce) + let bitPos := shl(and(0xff, nonce), 0x01) + mstore(callvalue(), 0x4fe02b44) // `ISignatureTransfer.nonceBitmap.selector` + mstore(0x20, address()) + mstore(0x40, wordPos) + if iszero(staticcall(gas(), _PERMIT2_ADDRESS, 0x1c, 0x44, callvalue(), 0x20)) { + returndatacopy(ptr, callvalue(), returndatasize()) + revert(ptr, returndatasize()) + } + let canceledNonces := mload(callvalue()) + if and(canceledNonces, bitPos) { + mstore(callvalue(), 0x756688fe) // `InvalidNonce.selector` + revert(0x1c, 0x04) + } + mstore(callvalue(), 0x3ff9dcb1) // `ISignatureTransfer.invalidateUnorderedNonces.selector` + mstore(returndatasize(), wordPos) + mstore(0x40, bitPos) + if iszero(call(gas(), _PERMIT2_ADDRESS, callvalue(), 0x1c, 0x44, codesize(), callvalue())) { + returndatacopy(ptr, callvalue(), returndatasize()) + revert(ptr, returndatasize()) + } + mstore(0x40, ptr) + } + } + + function _verifySimpleSignature(bytes32 signingHash, bytes calldata rvs, address owner_) private view { + assembly ("memory-safe") { + if xor(0x40, rvs.length) { + mstore(callvalue(), 0x4e487b71) // selector for `Panic(uint256)` + mstore(0x20, 0x32) // code for array out-of-bounds + revert(0x1c, 0x24) + } + + let ptr := mload(0x40) + + mstore(callvalue(), signingHash) + let vs := calldataload(add(0x20, rvs.offset)) + mstore(0x20, add(0x1b, shr(0xff, vs))) // v + mstore(0x40, calldataload(rvs.offset)) // r + mstore(0x60, and(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff, vs)) // s + + let recovered := mload(staticcall(gas(), 0x01, callvalue(), 0x80, 0x01, 0x20)) + if shl(0x60, xor(owner_, recovered)) { + mstore(callvalue(), 0x815e1d64) // `InvalidSigner.selector` + revert(0x1c, 0x04) + } + mstore(0x40, ptr) + mstore(0x60, callvalue()) + } + } + + function _eip712SigningHash(bytes32 structHash) private view returns (bytes32 signingHash) { + assembly ("memory-safe") { + let ptr := mload(0x40) + mstore(callvalue(), _DOMAIN_TYPEHASH) + mstore(0x20, _NAMEHASH) + mstore(0x40, chainid()) + mstore(0x60, address()) + mstore(0x20, keccak256(0x00, 0x80)) + mstore(callvalue(), 0x1901) + mstore(0x40, structHash) + signingHash := keccak256(0x1e, 0x42) + mstore(0x40, ptr) + mstore(0x60, 0x00) + } + } + + // 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 view returns (bytes32 signingHash) { + assembly ("memory-safe") { + let ptr := mload(0x40) + mstore(callvalue(), 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 + // containing 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) + + 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 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) + 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 } 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 + { + 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 srcData + let srcDataWord + let srcDataWordValue + { + srcDataWord := add(0x60, src) + srcDataWordValue := mload(srcDataWord) + let oom := shr(0x40, srcDataWordValue) + srcData := add(src, srcDataWordValue) + err := or(lt(lastWord, srcData), or(oom, err)) + } + + // decode the length of `src.data` and hash it + { + 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-hash the `Call` object into the `Call[]` array at `scratch[i]` + let typeHashWord := sub(src, 0x20) // not technically memory safe + let typeHashWordValue := mload(typeHashWord) + 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 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) + } + + 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), contextdepth) + mstore(add(0x60, scratch), nonce) + mstore(add(0x80, scratch), deadline) + + // final hashing + structHash := keccak256(scratch, 0xa0) + } + } + + /// @inheritdoc ICrossChainReceiverFactory + function metaTx( + IMultiCall.Call[] calldata /* calls */, + uint256 /* contextdepth */, + uint256 nonce, + 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)) { + if (relayer != _msgSender()) { + _permissionDenied(); + } + 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)); + + bytes memory data; + uint256 value; + if (owner_ != address(0)) { + bytes32 structHash; + (structHash, data, value) = _hashMultiCall(_msgData(), nonce, deadlineForHashing); + bytes32 signingHash = _nonEip712SigningHash(structHash); + + 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(callvalue(), 0x815e1d64) // `InvalidSigner.selector` + revert(0x1c, 0x04) + } + } + } else { + // `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. + owner_ = super.owner(); + nonce |= uint256(uint160(owner_)) << 96; + + bytes32 structHash; + (structHash, data, value) = _hashMultiCall(_msgData(), nonce, deadlineForHashing); + bytes32 signingHash = _eip712SigningHash(structHash); + _verifySimpleSignature(signingHash, signature, owner_); + } + + _useUnorderedNonce(nonce); + + unchecked { + if (address(this).balance < value) { + uint256 wrappedBalance; + IWrappedNative wnative = _WNATIVE; + bool hasWnative = _HAS_WNATIVE; + assembly ("memory-safe") { + mstore(0x00, 0x70a08231) // `IERC20.balanceOf.selector` + mstore(0x20, address()) + + if iszero(staticcall(gas(), wnative, 0x1c, 0x24, callvalue(), 0x20)) { + // this should never happen + revert(codesize(), callvalue()) + } + + wrappedBalance := mul(hasWnative, 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())) { + // this should never happen + revert(codesize(), callvalue()) + } + } + } + } + + 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, 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` + // 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(data, returndatasize()) } + + let rds := returndatasize() + + let multicallBalance := balance(MULTICALL) + if multicallBalance { + // get any excess native value back out of `MultiCall` + + 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, callvalue(), add(0x1c, ptr), 0x124, codesize(), callvalue())) { + // this should never happen + revert(codesize(), callvalue()) + } + } + + return(data, rds) } } @@ -394,23 +1029,20 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte if iszero( call(gas(), wnative, selfbalance(), codesize(), returndatasize(), codesize(), returndatasize()) ) { - let ptr := mload(0x40) - returndatacopy(ptr, 0x00, returndatasize()) - revert(ptr, returndatasize()) + // this should never happen + revert(codesize(), callvalue()) } } } } else { - if (_msgSender() != owner()) { - _permissionDenied(); - } + _requireOwner(); } selfdestruct(beneficiary); } /// 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. @@ -432,7 +1064,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(callvalue(), 0x40) // Hash both slots of scratch space. offset := add(0x20, offset) // Shift 1 word per cycle. @@ -451,13 +1083,13 @@ contract CrossChainReceiverFactory is ICrossChainReceiverFactory, MultiCallConte // derive creation salt mstore(0x14, originalOwner) - mstore(returndatasize(), root) - let salt := keccak256(returndatasize(), 0x34) + mstore(callvalue(), root) + let salt := keccak256(callvalue(), 0x34) // 0xff + factory + salt + hash(initCode) mstore(0x40, initHash) mstore(0x20, salt) - mstore(returndatasize(), factoryWithFF) + mstore(callvalue(), factoryWithFF) let computedAddress := keccak256(0x0b, 0x55) // restore clobbered memory @@ -564,7 +1196,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 @@ -573,14 +1205,14 @@ 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())) - { - let ptr := mload(0x40) - returndatacopy(ptr, 0x00, returndatasize()) - revert(ptr, returndatasize()) + if iszero( + call(gas(), wnative, callvalue(), codesize(), returndatasize(), codesize(), returndatasize()) + ) { + // this should never happen + revert(codesize(), calldatasize()) } } } diff --git a/src/interfaces/ICrossChainReceiverFactory.sol b/src/interfaces/ICrossChainReceiverFactory.sol index f8963cd6f..94b596f93 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); @@ -20,9 +21,28 @@ 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); + /// 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; 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())); + } } 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(); }