From dc16a6f7cd302d8c09bd2992606d03e5daaacf0c Mon Sep 17 00:00:00 2001 From: nicholaspai Date: Wed, 4 Dec 2024 10:44:29 -0500 Subject: [PATCH] Clean up ## CallByUser struct - remove delegateCodeHash from CallByUser since its always going to be the same for each delegated code - Add signature to CallByUser so XAccount can verify order was created by user - Add nonce to CallByUser to user can guarantee user op is unique ## OriginSettler - emit 7702 auth data in new event Requested7702Delegation - Add pendingOrder mapping for escrowing input Asset - Modify originData and orderId in ERC7683 order ## DestinationSettler - remove auth data parameter from DestinationSettler ## XAccount - Verify CallByUser.signature is signed CallByUser.calls from CallByUser.user --- README.md | 2 +- src/DestinationSettler.sol | 84 +++++++--------------- src/ERC7683Permit2Lib.sol | 45 +++++++++--- src/OriginSettler.sol | 139 ++++++++++++++++++++----------------- src/Structs.sol | 33 +++++++++ 5 files changed, 171 insertions(+), 132 deletions(-) create mode 100644 src/Structs.sol diff --git a/README.md b/README.md index 91a5c62..0826193 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ This repository contains contracts and scripts demonstrating this flow. - `OriginSettler`: Origin chain contract that user interacts with to open an ERC7683 cross-chain intent. The `open` function helps the user to form an ERC7683 intent correctly containing the `calldata` that the user wants to delegate to a filler to execute on the destination chain. `openFor` can be used to help a user pass their signed order to a filler off-chain and subsequently allows the filler to create the 7683 order on the user's behalf. Therefore, `openFor` allows the user to experience a totally gas-free experience from origin to destination chain. - The `open` functionality also optionally lets the user include a 7702 authorization that the user wants the filler to submit on-chain on their behalf. This can be used to allow the user to set the `code` of their destination chain EOA to the `XAccount` contract. - - In the 7683 order, includes the 7702 authorization data and the destination chain calldata in a [`FillInstruction`](https://eips.ethereum.org/EIPS/eip-7683#fillerdata) + - The 7683 order contains the destination chain calldata in a [`FillInstruction`](https://eips.ethereum.org/EIPS/eip-7683#fillerdata). If the 7702 delegation is a prerequisite for executing this calldata on the destination chain, then the filler should get the delegation data from the `OriginSettler` events. - `DestinationSettler`: Destination chain contract that filler interacts with to fulfill a ERC7683 cross-chain intent. The `fill` function is used by the `filler` to credit the user's EOA with any assets that they had deposited on the `OriginSettler` when initiating the 7683 intent and subsequently execute any `calldata` on behalf of the user that was included in the 7683 intent. - The `fill` function will delegate execution of `calldata` to the `XAccount` 7702-compatible proxy contract so it is a prerequisite that the user has already set their destination chain EOA's `code` to `XAccount` via a 7702 transaction. The authorization should submitted by the user or delegated to the filler to set. diff --git a/src/DestinationSettler.sol b/src/DestinationSettler.sol index bc8364b..41044bf 100644 --- a/src/DestinationSettler.sol +++ b/src/DestinationSettler.sol @@ -3,28 +3,8 @@ pragma solidity ^0.8.0; import {SignatureChecker} from "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {OriginSettler} from "./OriginSettler.sol"; import {GaslessCrossChainOrder} from "./ERC7683.sol"; -import "./ERC7683Permit2Lib.sol"; - -struct Asset { - address token; - uint256 amount; -} - -struct Call { - address target; - bytes callData; - uint256 value; -} - -struct CallByUser { - address user; // User who delegated calldata and funded assets on origin chain. - Asset asset; // token & amount, used to fund execution of calldata - uint64 chainId; // should match chain id where calls are to be executed - bytes32 delegateCodeHash; // expected code hash of the contract to which the user has delegated execution - Call[] calls; // calldata to execute -} +import {CallByUser, Call} from "./Structs.sol"; /** * @notice Destination chain entrypoint contract for fillers relaying cross chain message containing delegated @@ -44,9 +24,9 @@ contract DestinationSettler { // Called by filler, who sees ERC7683 intent emitted on origin chain // containing the callsByUser data to be executed following a 7702 delegation. - function fill(bytes32 orderId, bytes calldata originData, bytes calldata fillerData) external { - (CallByUser memory callsByUser, OriginSettler.EIP7702AuthData memory authData) = - abi.decode(originData, (CallByUser, OriginSettler.EIP7702AuthData)); + // @dev We don't use the last parameter `fillerData` in this function. + function fill(bytes32 orderId, bytes calldata originData, bytes calldata) external { + (CallByUser memory callsByUser) = abi.decode(originData, (CallByUser)); // Verify orderId? // require(orderId == keccak256(originData), "Wrong order data"); @@ -61,15 +41,14 @@ contract DestinationSettler { // TODO: Protect fillers from collisions with other fillers. Requires letting user set an exclusive relayer. // The following call will only succeed if the user has set a 7702 authorization to set its code - // equal to the XAccount contract. This 7702 auth data could have been included in the origin chain - // 7683 fillerData and subsequently could be submitted by the filler in a type 4 txn. The filler should have - // seen the calldata emitted in an `Open` ERC7683 event on the sending chain. - XAccount(payable(callsByUser.user)).xExecute(orderId, callsByUser, authData); + // equal to the XAccount contract. The filler should have seen any auth data emitted in an OriginSettler + // event on the sending chain. + XAccount(payable(callsByUser.user)).xExecute(orderId, callsByUser); // Perform any final steps required to prove that filler has successfully filled the ERC7683 intent. // For example, we could emit an event containing a unique hash of the fill that could be proved // on the origin chain via a receipt proof + RIP7755. - // e.g. emit Executed(userCalldata) + // e.g. emit Executed(orderId) } // Pull funds into this settlement contract as escrow and use to execute user's calldata. Escrowed @@ -77,8 +56,6 @@ contract DestinationSettler { // This step could be skipped by lightweight escrow systems that don't need to perform additional // validation on the filler's actions. function _fundAndApproveXAccount(CallByUser memory call) internal { - // TODO: Link the escrowed funds back to the user in case the delegation step fails, we don't want - // user to lose access to funds. IERC20(call.asset.token).safeTransferFrom(msg.sender, address(this), call.asset.amount); IERC20(call.asset.token).forceApprove(call.user, call.asset.amount); } @@ -89,8 +66,6 @@ contract DestinationSettler { /** * @notice Singleton contract used by all users who want to sign data on origin chain and delegate execution of * their calldata on this chain to this contract. - * @dev User must trust that this contract correctly verifies the user's cross chain signature as well as uses any - * 7702 delegations they want to delegate to a filler on this chain to bring on-chain. */ contract XAccount { using SafeERC20 for IERC20; @@ -101,18 +76,11 @@ contract XAccount { mapping(bytes32 => bool) public executionStatuses; // Entrypoint function to be called by DestinationSettler contract on this chain. Should pull funds - // to user's EOA and then execute calldata might require msg.sender = user EOA. - // Assume user has 7702-delegated code already to this contract, or that the user instructed the filler - // to submit the 7702 delegation data in the same transaction as the delegated calldata. - // All calldata and 7702 authorization data is assumed to have been emitted on the origin chain in a ERC7683 intent. - function xExecute( - bytes32 orderId, - CallByUser memory userCalls, - OriginSettler.EIP7702AuthData memory authorizationData - ) external { - // The user should have signed a data blob containing delegated calldata as well as any 7702 authorization - // transaction data they wanted the filler to submit on their behalf. - + // to user's EOA and then execute calldata. + // Assume user has 7702-delegated code already to this contract. + // All calldata and 7702 authorization data is assumed to have been emitted on the origin chain in am ERC7683 + // intent creation event. + function xExecute(bytes32 orderId, CallByUser memory userCalls) external { // TODO: Prevent userCalldata + signature from being replayed. require(!executionStatuses[orderId], "Already executed"); executionStatuses[orderId] = true; @@ -120,29 +88,29 @@ contract XAccount { // Verify that the user signed the data blob. _verifyCalls(userCalls); // Verify that any included 7702 authorization data is as expected. - _verify7702Delegation(userCalls, authorizationData); + _verify7702Delegation(); _fundUser(userCalls); + + // TODO: Should we allow user to handle case where the calls fail and they want to specify + // a fallback recipient? This might not be neccessary since the user will have pulled funds + // into their account so worst case they'll still have access to those funds. _attemptCalls(userCalls.calls); } - function _verifyCalls(CallByUser memory userCalls) internal view returns (bool) { + function _verifyCalls(CallByUser memory userCalls) internal view { // // TODO: How do we verify that userCalls.user is the expected user? require(userCalls.chainId == block.chainid); + require( + SignatureChecker.isValidSignatureNow( + userCalls.user, keccak256(abi.encode(userCalls.calls)), userCalls.signature + ) + ); } - function _verify7702Delegation(CallByUser memory userCalls, OriginSettler.EIP7702AuthData memory authorizationData) - internal - { + function _verify7702Delegation() internal { // TODO: We might not need this function at all, because if the authorization data requires that this contract // is set as the delegation code, then xExecute would fail if the auth data is not submitted by the filler. - // However, it might still be useful to verify that authorizationData includes some expected data like - // the authorization_list includes chainId=this and address=this. This might not be necessary though. - if (authorizationData.authlist.length == 0) { - return; - } - OriginSettler.Authorization memory authList = authorizationData.authlist[0]; - require(authList.chainId == block.chainid); - // TODO: Can we verify CallsByUser.delegateCodeHash for example? + // However, it might still be useful to verify that the delegate is set correctly, like checking EXTCODEHASH. } function _attemptCalls(Call[] memory calls) internal { diff --git a/src/ERC7683Permit2Lib.sol b/src/ERC7683Permit2Lib.sol index 2f25c44..d5e9684 100644 --- a/src/ERC7683Permit2Lib.sol +++ b/src/ERC7683Permit2Lib.sol @@ -1,19 +1,28 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.0; -import "./OriginSettler.sol"; -import "./DestinationSettler.sol"; -import "./IPermit2.sol"; +import {CallByUser, EIP7702AuthData} from "./Structs.sol"; import {GaslessCrossChainOrder} from "./ERC7683.sol"; bytes constant CALL_BY_USER_TYPE = abi.encodePacked( - "CallByUser(", "address user,", "Asset asset,", "uint64 chainId,", "bytes32 delegateCodeHash,", "Call[] calls)" + "CallByUser(", + "address user,", + "uint256 nonce", + "Asset asset,", + "uint64 chainId,", + "bytes signature,", + "Call[] calls)" ); bytes constant CALL_TYPE = abi.encodePacked("Call(", "address target,", "bytes callData,", "uint256 value)"); bytes constant ASSET_TYPE = abi.encodePacked("Asset(", "address token,", "uint256 amount)"); +bytes constant AUTHORIZATION_TYPE = + abi.encodePacked("Authorization(", "uint256 chainId", "address codeAddress", "uint256 nonce", "bytes signature)"); + +bytes constant EIP7702_AUTH_DATA_TYPE = abi.encodePacked("EIP7702AuthData(", "Authorization[] authlist)"); + bytes32 constant CALL_BY_USER_TYPE_HASH = keccak256(CALL_BY_USER_TYPE); library ERC7683Permit2Lib { @@ -29,19 +38,29 @@ library ERC7683Permit2Lib { "CallByUser orderData)" ); - bytes internal constant GASLESS_CROSS_CHAIN_ORDER_EIP712_TYPE = - abi.encodePacked(GASLESS_CROSS_CHAIN_ORDER_TYPE, CALL_BY_USER_TYPE, CALL_TYPE, ASSET_TYPE); + bytes internal constant GASLESS_CROSS_CHAIN_ORDER_EIP712_TYPE = abi.encodePacked( + GASLESS_CROSS_CHAIN_ORDER_TYPE, CALL_BY_USER_TYPE, CALL_TYPE, ASSET_TYPE, EIP7702_AUTH_DATA_TYPE + ); bytes32 internal constant GASLESS_CROSS_CHAIN_ORDER_TYPE_HASH = keccak256(GASLESS_CROSS_CHAIN_ORDER_EIP712_TYPE); string private constant TOKEN_PERMISSIONS_TYPE = "TokenPermissions(address token,uint256 amount)"; string internal constant PERMIT2_ORDER_TYPE = string( abi.encodePacked( - "GaslessCrossChainOrder witness)", CALL_BY_USER_TYPE, GASLESS_CROSS_CHAIN_ORDER_TYPE, TOKEN_PERMISSIONS_TYPE + "GaslessCrossChainOrder witness)", + AUTHORIZATION_TYPE, + EIP7702_AUTH_DATA_TYPE, + CALL_BY_USER_TYPE, + GASLESS_CROSS_CHAIN_ORDER_TYPE, + TOKEN_PERMISSIONS_TYPE ) ); // Hashes an order to get an order hash. Needed for permit2. - function hashOrder(GaslessCrossChainOrder memory order, bytes32 orderDataHash) internal pure returns (bytes32) { + function hashOrder(GaslessCrossChainOrder memory order, bytes32 userCallDataHash, bytes32 authDataHash) + internal + pure + returns (bytes32) + { return keccak256( abi.encode( GASLESS_CROSS_CHAIN_ORDER_TYPE_HASH, @@ -52,7 +71,8 @@ library ERC7683Permit2Lib { order.openDeadline, order.fillDeadline, order.orderDataType, - orderDataHash + userCallDataHash, + authDataHash ) ); } @@ -62,11 +82,16 @@ library ERC7683Permit2Lib { abi.encode( CALL_BY_USER_TYPE_HASH, userCallData.user, + userCallData.nonce, userCallData.asset, userCallData.chainId, - userCallData.delegateCodeHash, + userCallData.signature, userCallData.calls ) ); } + + function hashAuthData(EIP7702AuthData memory authData) internal pure returns (bytes32) { + return keccak256(abi.encode(EIP7702_AUTH_DATA_TYPE, authData.authlist, AUTHORIZATION_TYPE)); + } } diff --git a/src/OriginSettler.sol b/src/OriginSettler.sol index e0a89ad..bcfb5df 100644 --- a/src/OriginSettler.sol +++ b/src/OriginSettler.sol @@ -3,85 +3,58 @@ pragma solidity ^0.8.0; import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {GaslessCrossChainOrder, ResolvedCrossChainOrder, IOriginSettler, Output, FillInstruction} from "./ERC7683.sol"; -import {CallByUser, Call, Asset} from "./DestinationSettler.sol"; -import "./IPermit2.sol"; -import "./ERC7683Permit2Lib.sol"; +import {EIP7702AuthData, CallByUser, Call, Asset} from "./Structs.sol"; +import {IPermit2} from "./IPermit2.sol"; +import {ERC7683Permit2Lib} from "./ERC7683Permit2Lib.sol"; contract OriginSettler { using SafeERC20 for IERC20; IPermit2 public immutable PERMIT2 = IPermit2(address(0xf00d)); - // codeAddress will be set as the user's `code` on the `chainId` chain. - struct Authorization { - uint256 chainId; - address codeAddress; - uint256 nonce; - bytes signature; - } - - struct EIP7702AuthData { - Authorization[] authlist; - } - error WrongSettlementContract(); error WrongChainId(); error WrongOrderDataType(); error WrongExclusiveRelayer(); + event Requested7702Delegation(EIP7702AuthData authData); + bytes32 immutable ORDER_DATA_TYPE_HASH = keccak256("TODO"); - function openFor(GaslessCrossChainOrder calldata order, bytes calldata signature, bytes calldata originFillerData) - external - { - // TODO: Do we need to verify that signature is the signed order so that the filler can't just pass in any - // order data here? Or will this be implicitly handled by passing the signature into _processPermit2Order? + mapping(bytes32 => Asset) public pendingOrders; + + // @dev We don't use the last parameter `originFillerData` in this function. + function openFor(GaslessCrossChainOrder calldata order, bytes calldata permit2Signature, bytes calldata) external { ( ResolvedCrossChainOrder memory resolvedOrder, CallByUser memory calls, EIP7702AuthData memory authData, Asset memory inputAsset - ) = _resolveFor(order, originFillerData); - - // TODO: Support permit2 or approve+transferFrom flow or something else? - // // Verify Permit2 signature and pull user funds into this contract - _processPermit2Order(order, calls, inputAsset, signature); - - // TODO: Escrow funds in this contract and release post 7755 proof of settlement? Or use some other - // method. - // _setEscrowedFunds(inputAsset); + ) = _resolveFor(order); + + // Verify Permit2 signature and pull user funds into this contract. The signature should include + // the UserOp and any prerequisite EIP7702 delegation authorizations as witness data so we will doubly + // verify the user signed the data to be emitted as originData. + _processPermit2Order(order, calls, authData, inputAsset, permit2Signature); + + // TODO: Permit2 will pull assets into this contract, and they should only be releaseable to the filler + // on this chain once a proof of fill is submitted in a separate function. Ideally we can use RIP7755 + // to implement the storage proof escrow system. + require(pendingOrders[resolvedOrder.orderId].amount > 0, "Order already pending"); + pendingOrders[resolvedOrder.orderId] = inputAsset; + + // If a 7702 delegation is a prerequisite to executing the user's calldata on the destination chain, + // emit the authData here. + if (authData.authlist.length > 0) { + emit Requested7702Delegation(authData); + } + // The OpenEvent contains originData which is required to make the destination chain fill, so we only + // emit the user calls. emit IOriginSettler.Open(keccak256(resolvedOrder.fillInstructions[0].originData), resolvedOrder); } - function _processPermit2Order( - GaslessCrossChainOrder memory order, - CallByUser memory calls, - Asset memory inputAsset, - bytes memory signature - ) internal { - IPermit2.PermitTransferFrom memory permit = IPermit2.PermitTransferFrom({ - permitted: IPermit2.TokenPermissions({token: inputAsset.token, amount: inputAsset.amount}), - nonce: order.nonce, - deadline: order.openDeadline - }); - - IPermit2.SignatureTransferDetails memory signatureTransferDetails = - IPermit2.SignatureTransferDetails({to: address(this), requestedAmount: inputAsset.amount}); - - // Pull user funds. - PERMIT2.permitWitnessTransferFrom( - permit, - signatureTransferDetails, - order.user, - // Make sure signature includes the UserCallData. TODO: We probably need to include AuthData here too. - ERC7683Permit2Lib.hashOrder(order, ERC7683Permit2Lib.hashUserCallData(calls)), // witness data hash - ERC7683Permit2Lib.PERMIT2_ORDER_TYPE, // witness data type string - signature - ); - } - - function decode(bytes memory orderData) + function decode7683OrderData(bytes memory orderData) public pure returns (CallByUser memory calls, EIP7702AuthData memory authData, Asset memory asset) @@ -89,7 +62,7 @@ contract OriginSettler { return (abi.decode(orderData, (CallByUser, EIP7702AuthData, Asset))); } - function _resolveFor(GaslessCrossChainOrder calldata order, bytes calldata fillerData) + function _resolveFor(GaslessCrossChainOrder calldata order) internal view returns ( @@ -111,8 +84,7 @@ contract OriginSettler { revert WrongOrderDataType(); } - // TODO: Handle fillerData - (calls, authData, inputAsset) = decode(order.orderData); + (calls, authData, inputAsset) = decode7683OrderData(order.orderData); // Max outputs that filler should spend on destination chain. Output[] memory maxSpent = new Output[](1); @@ -133,8 +105,12 @@ contract OriginSettler { }); FillInstruction[] memory fillInstructions = new FillInstruction[](1); - // TODO: Decide what to set as the origin data. - bytes memory originData = abi.encode(calls, authData); + + // OriginData will be included on destination chain fill() and it should contain the data needed to execute the + // user's intended call. We don't include the authData here as the calldata execution will revert if the + // authData isn't submitted as a prerequisite to delegate the user's code. Instead, we emit the authData + // in this contract so that the filler submitting the destination chain calldata can use it. + bytes memory originData = abi.encode(calls); fillInstructions[0] = FillInstruction({ destinationChainId: calls.chainId, destinationSettler: _toBytes32(address(123)), // TODO: Should be address of destination settler for destination chain. @@ -149,8 +125,45 @@ contract OriginSettler { minReceived: minReceived, maxSpent: maxSpent, fillInstructions: fillInstructions, - orderId: keccak256(originData) // TODO: decide what to set as unique orderId. + orderId: _getOrderId(calls) + }); + } + + // This needs to be a unique representation of the user op. The CallByUser struct contains a nonce + // so the user can guarantee this order is unique by using the nonce+user combination. + function _getOrderId(CallByUser memory calls) internal pure returns (bytes32) { + return keccak256(abi.encode(calls)); + } + + function _processPermit2Order( + GaslessCrossChainOrder memory order, + CallByUser memory calls, + EIP7702AuthData memory authData, + Asset memory inputAsset, + bytes memory signature + ) internal { + IPermit2.PermitTransferFrom memory permit = IPermit2.PermitTransferFrom({ + permitted: IPermit2.TokenPermissions({token: inputAsset.token, amount: inputAsset.amount}), + nonce: order.nonce, + deadline: order.openDeadline }); + + IPermit2.SignatureTransferDetails memory signatureTransferDetails = + IPermit2.SignatureTransferDetails({to: address(this), requestedAmount: inputAsset.amount}); + + // Pull user funds into this contract. + PERMIT2.permitWitnessTransferFrom( + permit, + signatureTransferDetails, + order.user, + // User should have signed a permit2 blob including the destination chain UserOp and any prerequisite + // EIP7702 delegation authorizations. + ERC7683Permit2Lib.hashOrder( + order, ERC7683Permit2Lib.hashUserCallData(calls), ERC7683Permit2Lib.hashAuthData(authData) + ), // witness data hash + ERC7683Permit2Lib.PERMIT2_ORDER_TYPE, // witness data type string + signature + ); } function _toBytes32(address input) internal pure returns (bytes32) { diff --git a/src/Structs.sol b/src/Structs.sol new file mode 100644 index 0000000..d37a24d --- /dev/null +++ b/src/Structs.sol @@ -0,0 +1,33 @@ +pragma solidity ^0.8.0; + +struct Authorization { + uint256 chainId; + address codeAddress; + uint256 nonce; + bytes signature; +} + +struct EIP7702AuthData { + Authorization[] authlist; +} + +struct Asset { + address token; + uint256 amount; +} + +struct Call { + address target; + bytes callData; + uint256 value; +} + +struct CallByUser { + address user; // User who delegated calldata and funded assets on origin chain. + uint256 nonce; // Unique nonce for this user call to prevent replay. Set by user on origin chain so + // there is no re-org risk to this value. + Asset asset; // token & amount, used to fund execution of calldata + uint64 chainId; // should match chain id where calls are to be executed + bytes signature; // Signed calldata, to be used by the XAccount contract to verify the user's ordered the calldata. + Call[] calls; // calldata to execute +}