Skip to content

Commit

Permalink
Clean up
Browse files Browse the repository at this point in the history
## 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
  • Loading branch information
nicholaspai committed Dec 4, 2024
1 parent 0e96c85 commit dc16a6f
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 132 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
84 changes: 26 additions & 58 deletions src/DestinationSettler.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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");

Expand All @@ -61,24 +41,21 @@ 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
// funds will be paid back to filler after this contract successfully verifies the settled intent.
// 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);
}
Expand All @@ -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;
Expand All @@ -101,48 +76,41 @@ 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;

// 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 {
Expand Down
45 changes: 35 additions & 10 deletions src/ERC7683Permit2Lib.sol
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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,
Expand All @@ -52,7 +71,8 @@ library ERC7683Permit2Lib {
order.openDeadline,
order.fillDeadline,
order.orderDataType,
orderDataHash
userCallDataHash,
authDataHash
)
);
}
Expand All @@ -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));
}
}
Loading

0 comments on commit dc16a6f

Please sign in to comment.