diff --git a/README.md b/README.md index d192e50..91a5c62 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,8 @@ This repository contains contracts and scripts demonstrating this flow. ## On-chain Components -- `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. - - The `open` function 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. +- `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) - `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 diff --git a/script/Counter.s.sol b/script/Counter.s.sol deleted file mode 100644 index cdc1fe9..0000000 --- a/script/Counter.s.sol +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Script, console} from "forge-std/Script.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterScript is Script { - Counter public counter; - - function setUp() public {} - - function run() public { - vm.startBroadcast(); - - counter = new Counter(); - - vm.stopBroadcast(); - } -} diff --git a/src/Counter.sol b/src/Counter.sol deleted file mode 100644 index aded799..0000000 --- a/src/Counter.sol +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -contract Counter { - uint256 public number; - - function setNumber(uint256 newNumber) public { - number = newNumber; - } - - function increment() public { - number++; - } -} diff --git a/src/DestinationSettler.sol b/src/DestinationSettler.sol index 0b40680..ddc220a 100644 --- a/src/DestinationSettler.sol +++ b/src/DestinationSettler.sol @@ -6,7 +6,7 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {OriginSettler} from "./OriginSettler.sol"; struct Asset { - IERC20 token; + address token; uint256 amount; } @@ -77,8 +77,8 @@ contract DestinationSettler { function _fundUserAndApproveXAccount(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. - call.asset.token.safeTransferFrom(msg.sender, address(this), call.asset.amount); - call.asset.token.forceApprove(call.user, call.asset.amount); + IERC20(call.asset.token).safeTransferFrom(msg.sender, address(this), call.asset.amount); + IERC20(call.asset.token).forceApprove(call.user, call.asset.amount); } } @@ -164,7 +164,7 @@ contract XAccount { } function _fundUser(CallByUser memory call) internal { - call.asset.token.safeTransferFrom(msg.sender, call.user, call.asset.amount); + IERC20(call.asset.token).safeTransferFrom(msg.sender, call.user, call.asset.amount); } // Used if the caller is trying to unwrap the native token to this contract. diff --git a/src/ERC7683Permit2Lib.sol b/src/ERC7683Permit2Lib.sol new file mode 100644 index 0000000..2f25c44 --- /dev/null +++ b/src/ERC7683Permit2Lib.sol @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "./OriginSettler.sol"; +import "./DestinationSettler.sol"; +import "./IPermit2.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)" +); + +bytes constant CALL_TYPE = abi.encodePacked("Call(", "address target,", "bytes callData,", "uint256 value)"); + +bytes constant ASSET_TYPE = abi.encodePacked("Asset(", "address token,", "uint256 amount)"); + +bytes32 constant CALL_BY_USER_TYPE_HASH = keccak256(CALL_BY_USER_TYPE); + +library ERC7683Permit2Lib { + bytes internal constant GASLESS_CROSS_CHAIN_ORDER_TYPE = abi.encodePacked( + "GaslessCrossChainOrder(", + "address originSettler,", + "address user,", + "uint256 nonce,", + "uint256 originChainId,", + "uint32 openDeadline,", + "uint32 fillDeadline,", + "bytes32 orderDataType,", + "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); + 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 + ) + ); + + // Hashes an order to get an order hash. Needed for permit2. + function hashOrder(GaslessCrossChainOrder memory order, bytes32 orderDataHash) internal pure returns (bytes32) { + return keccak256( + abi.encode( + GASLESS_CROSS_CHAIN_ORDER_TYPE_HASH, + order.originSettler, + order.user, + order.nonce, + order.originChainId, + order.openDeadline, + order.fillDeadline, + order.orderDataType, + orderDataHash + ) + ); + } + + function hashUserCallData(CallByUser memory userCallData) internal pure returns (bytes32) { + return keccak256( + abi.encode( + CALL_BY_USER_TYPE_HASH, + userCallData.user, + userCallData.asset, + userCallData.chainId, + userCallData.delegateCodeHash, + userCallData.calls + ) + ); + } +} diff --git a/src/IPermit2.sol b/src/IPermit2.sol new file mode 100644 index 0000000..61df041 --- /dev/null +++ b/src/IPermit2.sol @@ -0,0 +1,30 @@ +pragma solidity ^0.8.0; + +interface IPermit2 { + struct TokenPermissions { + address token; + uint256 amount; + } + + struct PermitTransferFrom { + TokenPermissions permitted; + uint256 nonce; + uint256 deadline; + } + + struct SignatureTransferDetails { + address to; + uint256 requestedAmount; + } + + function permitWitnessTransferFrom( + PermitTransferFrom memory permit, + SignatureTransferDetails calldata transferDetails, + address owner, + bytes32 witness, + string calldata witnessTypeString, + bytes calldata signature + ) external; + + function transferFrom(address from, address to, uint160 amount, address token) external; +} diff --git a/src/OriginSettler.sol b/src/OriginSettler.sol index e0bda1a..6a45eba 100644 --- a/src/OriginSettler.sol +++ b/src/OriginSettler.sol @@ -4,10 +4,14 @@ 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"; 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; @@ -20,11 +24,6 @@ contract OriginSettler { Authorization[] authlist; } - struct InputAsset { - IERC20 token; - uint256 amount; - } - error WrongSettlementContract(); error WrongChainId(); error WrongOrderDataType(); @@ -35,16 +34,18 @@ contract OriginSettler { 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? ( ResolvedCrossChainOrder memory resolvedOrder, CallByUser memory calls, EIP7702AuthData memory authData, - InputAsset memory inputAsset + 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, acrossOrderData, signature); + _processPermit2Order(order, calls, inputAsset, signature); // TODO: Escrow funds in this contract and release post 7755 proof of settlement? Or use some other // method. @@ -53,12 +54,38 @@ contract OriginSettler { 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, + ERC7683Permit2Lib.hashOrder(order, ERC7683Permit2Lib.hashUserCallData(calls)), // witness data hash + ERC7683Permit2Lib.PERMIT2_ORDER_TYPE, // witness data type string + signature + ); + } + function decode(bytes memory orderData) public pure - returns (CallByUser memory calls, EIP7702AuthData memory authData, InputAsset memory asset) + returns (CallByUser memory calls, EIP7702AuthData memory authData, Asset memory asset) { - return (abi.decode(orderData, (CallByUser, EIP7702AuthData, InputAsset))); + return (abi.decode(orderData, (CallByUser, EIP7702AuthData, Asset))); } function _resolveFor(GaslessCrossChainOrder calldata order, bytes calldata fillerData) @@ -68,7 +95,7 @@ contract OriginSettler { ResolvedCrossChainOrder memory resolvedOrder, CallByUser memory calls, EIP7702AuthData memory authData, - InputAsset memory inputAsset + Asset memory inputAsset ) { if (order.originSettler != address(this)) { @@ -89,7 +116,7 @@ contract OriginSettler { // Max outputs that filler should spend on destination chain. Output[] memory maxSpent = new Output[](1); maxSpent[0] = Output({ - token: _toBytes32(address(calls.asset.token)), + token: _toBytes32(calls.asset.token), amount: calls.asset.amount, recipient: _toBytes32(calls.user), chainId: calls.chainId @@ -98,7 +125,7 @@ contract OriginSettler { // Minimum outputs that must be pulled from caller on this chain. Output[] memory minReceived = new Output[](1); minReceived[0] = Output({ - token: _toBytes32(address(inputAsset.token)), + token: _toBytes32(inputAsset.token), amount: inputAsset.amount, recipient: _toBytes32(msg.sender), // We assume that msg.sender is filler and wants to be repaid on this chain. chainId: block.chainid diff --git a/test/Counter.t.sol b/test/Counter.t.sol deleted file mode 100644 index 54b724f..0000000 --- a/test/Counter.t.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import {Test, console} from "forge-std/Test.sol"; -import {Counter} from "../src/Counter.sol"; - -contract CounterTest is Test { - Counter public counter; - - function setUp() public { - counter = new Counter(); - counter.setNumber(0); - } - - function test_Increment() public { - counter.increment(); - assertEq(counter.number(), 1); - } - - function testFuzz_SetNumber(uint256 x) public { - counter.setNumber(x); - assertEq(counter.number(), x); - } -}