From e952c9dc36cf53b4648d8bdfaed1d97d06492f82 Mon Sep 17 00:00:00 2001 From: Jack Chuma Date: Mon, 10 Feb 2025 15:24:26 -0500 Subject: [PATCH] improve user op handling in outbox, remove private key from script management, deploy mock account --- contracts/Makefile | 35 +- contracts/script/HelperConfig.s.sol | 26 +- contracts/script/actions/SubmitRequest.s.sol | 2 +- contracts/script/actions/SubmitToInbox.s.sol | 3 +- contracts/script/actions/SubmitUserOp.s.sol | 111 ++++++ contracts/src/RRC7755Base.sol | 6 +- contracts/src/RRC7755Inbox.sol | 6 +- contracts/src/RRC7755Outbox.sol | 393 ++++++++++++------- contracts/test/RRC7755Inbox.t.sol | 7 +- contracts/test/RRC7755Outbox.t.sol | 234 ++++++----- 10 files changed, 534 insertions(+), 289 deletions(-) create mode 100644 contracts/script/actions/SubmitUserOp.s.sol diff --git a/contracts/Makefile b/contracts/Makefile index e8e75c8..ccd69c1 100644 --- a/contracts/Makefile +++ b/contracts/Makefile @@ -1,16 +1,12 @@ -include .env -.PHONY: test +# ACCOUNT=mock-account-deployer +ACCOUNT=testnet-admin ARBITRUM_REQUEST_HASH = 0xd2a7d400b8bf591dfa337aced6d4bab04e979ef9d183c9fc30bc1c6c0ce552aa -OPTIMISM_REQUEST_HASH = 0xe38ad8c9e84178325f28799eb3aaae72551b2eea7920c43d88854edd350719f5 FULFILLER_ADDRESS = 0x23214A0864FC0014CAb6030267738F01AFfdd547 MOCK_VERIFIER_ADDRESS = 0xdac62f96404AB882F5a61CFCaFb0C470a19FC514 - -# Default Anvil Keys -PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 -CHAIN_A_KEY=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a -CHAIN_B_KEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d +MOCK_ACCOUNT = 0x2c4d5B2d8B7ba9e15F09Da8fD455E312bF774Eeb CHAIN_A_URL=http://localhost:8546 CHAIN_B_URL=http://localhost:8547 @@ -22,31 +18,46 @@ BASE_RPC = $(BASE_SEPOLIA_RPC) SUBMIT_REQUEST_RPC = $(OPTIMISM_RPC) OP_STACK_RPC = $(BASE_RPC) +.PHONY: test test: forge fmt forge test +.PHONY: coverage coverage: forge fmt forge coverage +.PHONY: deploy-mock deploy-mock: - forge create --rpc-url $(ARBITRUM_RPC) --private-key $(PRIVATE_KEY) test/mocks/MockVerifier.sol:MockVerifier --broadcast -vvvv + forge create --rpc-url $(ARBITRUM_RPC) --account $(ACCOUNT) test/mocks/MockVerifier.sol:MockVerifier --broadcast -vvvv + +.PHONY: deploy-mock-account +deploy-mock-account: + forge create --rpc-url $(ARBITRUM_RPC) --account $(ACCOUNT) test/mocks/MockAccount.sol:MockAccount --broadcast -vvvv + forge create --rpc-url $(OPTIMISM_RPC) --account $(ACCOUNT) test/mocks/MockAccount.sol:MockAccount --broadcast -vvvv + forge create --rpc-url $(BASE_RPC) --account $(ACCOUNT) test/mocks/MockAccount.sol:MockAccount --broadcast -vvvv +.PHONY: read-mock read-mock: cast call $(MOCK_VERIFIER_ADDRESS) "getFulfillmentInfo(bytes32)(uint96,address)" $(ARBITRUM_REQUEST_HASH) --rpc-url $(ARBITRUM_RPC) +.PHONY: set-mock set-mock: - cast send $(MOCK_VERIFIER_ADDRESS) "storeFulfillmentInfo(bytes32,address)" $(ARBITRUM_REQUEST_HASH) $(FULFILLER_ADDRESS) --rpc-url $(ARBITRUM_RPC) --private-key $(PRIVATE_KEY) + cast send $(MOCK_VERIFIER_ADDRESS) "storeFulfillmentInfo(bytes32,address)" $(ARBITRUM_REQUEST_HASH) $(FULFILLER_ADDRESS) --rpc-url $(ARBITRUM_RPC) --account $(ACCOUNT) +.PHONY: deploy-arbitrum-sepolia deploy-arbitrum-sepolia: forge script script/chains/DeployArbitrum.s.sol:DeployArbitrum --rpc-url $(ARBITRUM_RPC) --broadcast -vvvv +.PHONY: deploy-op-stack deploy-op-stack: - PRIVATE_KEY=$(PRIVATE_KEY) forge script script/chains/DeployBase.s.sol:DeployBase --rpc-url $(OP_STACK_RPC) --broadcast -vvvv + forge script script/chains/DeployBase.s.sol:DeployBase --rpc-url $(OP_STACK_RPC) --account $(ACCOUNT) --broadcast -vvvv +.PHONY: submit-request submit-request: - PRIVATE_KEY=$(PRIVATE_KEY) forge script script/actions/SubmitRequest.s.sol:SubmitRequest --rpc-url $(SUBMIT_REQUEST_RPC) --broadcast -vvvv + forge script script/actions/SubmitRequest.s.sol:SubmitRequest --rpc-url $(SUBMIT_REQUEST_RPC) --account $(ACCOUNT) --broadcast -vvvv +.PHONY: fulfill-request fulfill-request: - PRIVATE_KEY=$(PRIVATE_KEY) forge script script/actions/SubmitToInbox.s.sol:SubmitToInbox --rpc-url $(OPTIMISM_RPC) --broadcast -vvvv + forge script script/actions/SubmitToInbox.s.sol:SubmitToInbox --rpc-url $(OPTIMISM_RPC) --account $(ACCOUNT) --broadcast -vvvv diff --git a/contracts/script/HelperConfig.s.sol b/contracts/script/HelperConfig.s.sol index 3162245..5b263f1 100644 --- a/contracts/script/HelperConfig.s.sol +++ b/contracts/script/HelperConfig.s.sol @@ -5,6 +5,8 @@ import {Script} from "forge-std/Script.sol"; import {EntryPoint} from "account-abstraction/core/EntryPoint.sol"; +import {MockAccount} from "../test/mocks/MockAccount.sol"; + contract HelperConfig is Script { struct NetworkConfig { uint256 chainId; @@ -14,8 +16,9 @@ contract HelperConfig is Script { address inbox; address l2Oracle; address shoyuBashi; - uint256 deployerKey; address entryPoint; + address smartAccount; + string rpcUrl; } uint256 public DEFAULT_ANVIL_PRIVATE_KEY = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80; @@ -48,8 +51,9 @@ contract HelperConfig is Script { inbox: 0xAF8e568F4E3105e1D8818B26dCA57CD4bd753695, l2Oracle: 0x042B2E6C5E99d4c521bd49beeD5E99651D9B0Cf4, shoyuBashi: 0xce8b068D4F7F2eb3bDAFa72eC3C4feE78CF9Ccf7, - deployerKey: vm.envUint("PRIVATE_KEY"), - entryPoint: 0x0000000071727De22E5E9d8BAf0edAc6f37da032 + entryPoint: 0x0000000071727De22E5E9d8BAf0edAc6f37da032, + smartAccount: 0x2c4d5B2d8B7ba9e15F09Da8fD455E312bF774Eeb, + rpcUrl: vm.envString("ARBITRUM_SEPOLIA_RPC") }); } @@ -62,8 +66,9 @@ contract HelperConfig is Script { inbox: 0x8e993853C303288f4fcd138E180E31a3c798E4F9, l2Oracle: 0x4C8BA32A5DAC2A720bb35CeDB51D6B067D104205, shoyuBashi: 0x6602dc9b6bd964C2a11BBdA9B2275308D1Bbc14f, - deployerKey: vm.envUint("PRIVATE_KEY"), - entryPoint: 0x0000000071727De22E5E9d8BAf0edAc6f37da032 + entryPoint: 0x0000000071727De22E5E9d8BAf0edAc6f37da032, + smartAccount: 0x2c4d5B2d8B7ba9e15F09Da8fD455E312bF774Eeb, + rpcUrl: vm.envString("BASE_SEPOLIA_RPC") }); } @@ -76,13 +81,15 @@ contract HelperConfig is Script { inbox: 0x9435B271fB6b525B87171F92379A5c85fEF4d4cB, l2Oracle: 0x218CD9489199F321E1177b56385d333c5B598629, shoyuBashi: 0x7237bb8d1d38DF8b473b5A38eD90088AF162ad8e, - deployerKey: vm.envUint("PRIVATE_KEY"), - entryPoint: 0x0000000071727De22E5E9d8BAf0edAc6f37da032 + entryPoint: 0x0000000071727De22E5E9d8BAf0edAc6f37da032, + smartAccount: 0x2c4d5B2d8B7ba9e15F09Da8fD455E312bF774Eeb, + rpcUrl: vm.envString("OPTIMISM_SEPOLIA_RPC") }); } function getLocalConfig() public returns (NetworkConfig memory) { EntryPoint entryPoint = new EntryPoint(); + MockAccount smartAccount = new MockAccount(); return NetworkConfig({ chainId: LOCAL_CHAIN_ID, @@ -92,8 +99,9 @@ contract HelperConfig is Script { inbox: address(0), l2Oracle: address(0), shoyuBashi: address(0), - deployerKey: 0, - entryPoint: address(entryPoint) + entryPoint: address(entryPoint), + smartAccount: address(smartAccount), + rpcUrl: "" }); } diff --git a/contracts/script/actions/SubmitRequest.s.sol b/contracts/script/actions/SubmitRequest.s.sol index 610d6be..98ac41d 100644 --- a/contracts/script/actions/SubmitRequest.s.sol +++ b/contracts/script/actions/SubmitRequest.s.sol @@ -36,7 +36,7 @@ contract SubmitRequest is Script, RRC7755Base { (bytes32 destinationChain, bytes32 receiver, Call[] memory calls, bytes[] memory attributes) = _initMessage(destinationChainId, duration); - vm.startBroadcast(config.deployerKey); + vm.startBroadcast(); outbox.sendMessage{value: 0.0002 ether}(destinationChain, receiver, abi.encode(calls), attributes); vm.stopBroadcast(); } diff --git a/contracts/script/actions/SubmitToInbox.s.sol b/contracts/script/actions/SubmitToInbox.s.sol index dde7d23..d897844 100644 --- a/contracts/script/actions/SubmitToInbox.s.sol +++ b/contracts/script/actions/SubmitToInbox.s.sol @@ -17,13 +17,12 @@ contract SubmitToInbox is Script, RRC7755Base { bytes4 internal constant _SHOYU_BASHI_ATTRIBUTE_SELECTOR = 0xda07e15d; // shoyuBashi(bytes32) function run() external { - uint256 pk = vm.envUint("PRIVATE_KEY"); RRC7755Inbox inbox = RRC7755Inbox(payable(0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512)); address fulfiller = 0x23214A0864FC0014CAb6030267738F01AFfdd547; (bytes32 sourceChain, bytes32 sender, bytes memory payload, bytes[] memory attributes) = _initMessage(); - vm.startBroadcast(pk); + vm.startBroadcast(); inbox.fulfill(sourceChain, sender, payload, attributes, fulfiller); vm.stopBroadcast(); } diff --git a/contracts/script/actions/SubmitUserOp.s.sol b/contracts/script/actions/SubmitUserOp.s.sol new file mode 100644 index 0000000..211d24c --- /dev/null +++ b/contracts/script/actions/SubmitUserOp.s.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import {Script} from "forge-std/Script.sol"; + +import {PackedUserOperation} from "account-abstraction/interfaces/PackedUserOperation.sol"; +import {EntryPoint} from "account-abstraction/core/EntryPoint.sol"; + +import {GlobalTypes} from "../../src/libraries/GlobalTypes.sol"; +import {RRC7755Base} from "../../src/RRC7755Base.sol"; +import {RRC7755Outbox} from "../../src/RRC7755Outbox.sol"; +import {HelperConfig} from "../HelperConfig.s.sol"; + +import {MockAccount} from "../../test/mocks/MockAccount.sol"; + +contract SubmitUserOp is Script, RRC7755Base { + using GlobalTypes for address; + + bytes4 internal constant _REWARD_ATTRIBUTE_SELECTOR = 0xa362e5db; // reward(bytes32,uint256) rewardAsset, rewardAmount + bytes4 internal constant _DELAY_ATTRIBUTE_SELECTOR = 0x84f550e0; // delay(uint256,uint256) finalityDelaySeconds, expiry + bytes4 internal constant _L2_ORACLE_ATTRIBUTE_SELECTOR = 0x7ff7245a; // l2Oracle(address) + bytes4 internal constant _SHOYU_BASHI_ATTRIBUTE_SELECTOR = 0xda07e15d; // shoyuBashi(bytes32) + bytes4 internal constant _DESTINATION_CHAIN_SELECTOR = 0xdff49bf1; // destinationChain(bytes32) + bytes32 private constant _NATIVE_ASSET = 0x000000000000000000000000eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee; + + HelperConfig public helperConfig; + + constructor() { + helperConfig = new HelperConfig(); + } + + function run() external { + HelperConfig.NetworkConfig memory config = helperConfig.getConfig(block.chainid); + + address outboxAddr = config.opStackOutbox; + uint256 destinationChainId = helperConfig.BASE_SEPOLIA_CHAIN_ID(); + uint256 duration = 1 weeks; + + RRC7755Outbox outbox = RRC7755Outbox(outboxAddr); + + (bytes32 destinationChain, bytes32 receiver, bytes memory payload) = _initMessage(destinationChainId, duration); + + vm.createSelectFork(config.rpcUrl); + + vm.startBroadcast(); + outbox.sendMessage{value: 0.0002 ether}(destinationChain, receiver, payload, new bytes[](0)); + vm.stopBroadcast(); + } + + function _initMessage(uint256 destinationChainId, uint256 duration) + private + returns (bytes32, bytes32, bytes memory) + { + HelperConfig.NetworkConfig memory dstConfig = helperConfig.getConfig(destinationChainId); + // HelperConfig.NetworkConfig memory srcConfig = helperConfig.getConfig(block.chainid); + + address ethAddress = address(0); + + uint128 verificationGasLimit = 100000; + uint128 callGasLimit = 100000; + uint128 maxPriorityFeePerGas = 100000; + uint128 maxFeePerGas = 100000; + + vm.createSelectFork(dstConfig.rpcUrl); + uint256 nonce = EntryPoint(payable(dstConfig.entryPoint)).getNonce(dstConfig.smartAccount, 0); + + bytes32 destinationChain = bytes32(destinationChainId); + bytes32 receiver = dstConfig.entryPoint.addressToBytes32(); + bytes[] memory attributes = new bytes[](3); + + attributes[0] = abi.encodeWithSelector(_REWARD_ATTRIBUTE_SELECTOR, _NATIVE_ASSET, 0.0002 ether); + attributes[1] = abi.encodeWithSelector(_DELAY_ATTRIBUTE_SELECTOR, duration, block.timestamp + 2 weeks); + attributes[2] = abi.encodeWithSelector(_L2_ORACLE_ATTRIBUTE_SELECTOR, dstConfig.l2Oracle); + // attributes[2] = abi.encodeWithSelector(_SHOYU_BASHI_ATTRIBUTE_SELECTOR, srcConfig.shoyuBashi); + // attributes[3] = abi.encodeWithSelector(_DESTINATION_CHAIN_SELECTOR, destinationChain); + + PackedUserOperation memory userOp = PackedUserOperation({ + sender: dstConfig.smartAccount, + nonce: nonce + 1, + initCode: "", + callData: abi.encodeWithSelector(MockAccount.executeUserOp.selector, address(dstConfig.inbox), ethAddress), + accountGasLimits: bytes32(abi.encodePacked(verificationGasLimit, callGasLimit)), + preVerificationGas: 100000, + gasFees: bytes32(abi.encodePacked(maxPriorityFeePerGas, maxFeePerGas)), + paymasterAndData: _encodePaymasterAndData(dstConfig.inbox, attributes, ethAddress), + signature: "" + }); + + return (destinationChain, receiver, abi.encode(userOp)); + } + + function _encodePaymasterAndData(address inbox, bytes[] memory attributes, address ethAddress) + private + pure + returns (bytes memory) + { + address precheck = address(0); + uint256 ethAmount = 0.0001 ether; + uint128 paymasterVerificationGasLimit = 100000; + uint128 paymasterPostOpGasLimit = 100000; + return abi.encodePacked( + inbox, + paymasterVerificationGasLimit, + paymasterPostOpGasLimit, + abi.encode(ethAddress, ethAmount, precheck, attributes) + ); + } + + // Including to block from coverage report + function test() external {} +} diff --git a/contracts/src/RRC7755Base.sol b/contracts/src/RRC7755Base.sol index 04e17c6..1a2530d 100644 --- a/contracts/src/RRC7755Base.sol +++ b/contracts/src/RRC7755Base.sol @@ -20,10 +20,6 @@ contract RRC7755Base { /// @notice The selector for the precheck attribute bytes4 internal constant _PRECHECK_ATTRIBUTE_SELECTOR = 0xbef86027; // precheck(bytes32) - /// @notice The selector for the isUserOp attribute. Used to designate a request designated to be a destination - /// chain ERC-4337 User Operation - bytes4 internal constant _USER_OP_ATTRIBUTE_SELECTOR = 0xd45448dd; // isUserOp(bool) - /// @notice This error is thrown if an attribute is not found in the attributes array /// /// @param selector The selector of the attribute that was not found @@ -48,7 +44,7 @@ contract RRC7755Base { bytes32 receiver, bytes calldata payload, bytes[] calldata attributes - ) public pure returns (bytes32) { + ) public view virtual returns (bytes32) { return keccak256(abi.encode(sourceChain, sender, destinationChain, receiver, payload, attributes)); } diff --git a/contracts/src/RRC7755Inbox.sol b/contracts/src/RRC7755Inbox.sol index cf29d71..cc2492e 100644 --- a/contracts/src/RRC7755Inbox.sol +++ b/contracts/src/RRC7755Inbox.sol @@ -154,13 +154,11 @@ contract RRC7755Inbox is RRC7755Base, Paymaster { } function _processAttributes(bytes[] calldata attributes) private pure returns (bool, address) { - bool isUserOp; + bool isUserOp = attributes.length == 0; bytes32 precheckContract; for (uint256 i; i < attributes.length; i++) { - if (bytes4(attributes[i]) == _USER_OP_ATTRIBUTE_SELECTOR) { - isUserOp = abi.decode(attributes[i][4:], (bool)); - } else if (bytes4(attributes[i]) == _PRECHECK_ATTRIBUTE_SELECTOR) { + if (bytes4(attributes[i]) == _PRECHECK_ATTRIBUTE_SELECTOR) { precheckContract = abi.decode(attributes[i][4:], (bytes32)); } } diff --git a/contracts/src/RRC7755Outbox.sol b/contracts/src/RRC7755Outbox.sol index ebd7fcc..49fc2a3 100644 --- a/contracts/src/RRC7755Outbox.sol +++ b/contracts/src/RRC7755Outbox.sol @@ -59,9 +59,6 @@ abstract contract RRC7755Outbox is RRC7755Base { /// @notice The duration, in excess of CrossChainRequest.expiry, which must pass before a request can be canceled uint256 public constant CANCEL_DELAY_SECONDS = 1 days; - /// @notice The expected minimum length of the attributes array supplied to `sendMessage` - uint256 private constant _EXPECTED_ATTRIBUTE_LENGTH = 2; - /// @notice An incrementing nonce value to ensure no two `CrossChainRequest` can be exactly the same uint256 private _nonce; @@ -131,19 +128,18 @@ abstract contract RRC7755Outbox is RRC7755Base { /// @param selector The selector of the unsupported attribute error UnsupportedAttribute(bytes4 selector); - /// @notice This error is thrown if the attribute length supplied to `sendMessage` is not equal to the expected - /// length - /// - /// @param expected The expected length of the attributes - /// @param actual The actual length of the attributes - error InvalidAttributeLength(uint256 expected, uint256 actual); - /// @notice This error is thrown if a required attribute is missing from the global attributes array for a 7755 /// request /// /// @param selector The selector of the missing attribute error MissingRequiredAttribute(bytes4 selector); + /// @notice This error is thrown if the passed in nonce is incorrect + error InvalidNonce(); + + /// @notice This error is thrown if the passed in requester is not equal to msg.sender + error InvalidRequester(); + /// @notice Initiates the sending of a 7755 request containing a single message /// /// @custom:reverts If the attributes array length is less than 3 @@ -165,17 +161,20 @@ abstract contract RRC7755Outbox is RRC7755Base { bytes calldata payload, bytes[] calldata attributes ) external payable returns (bytes32) { - (bytes[] memory expandedAttributes, bool isUserOp) = _processAttributes(attributes); + if (attributes.length == 0) { + bytes[] memory userOpAttributes = _getUserOpAttributes(payload); + this.processAttributes(userOpAttributes, msg.sender, msg.value); + } else { + this.processAttributes(attributes, msg.sender, msg.value); + } + bytes32 sender = address(this).addressToBytes32(); bytes32 sourceChain = bytes32(block.chainid); - bytes32 messageId = - this.getRequestId(sourceChain, sender, destinationChain, receiver, payload, expandedAttributes, isUserOp); + bytes32 messageId = getRequestId(sourceChain, sender, destinationChain, receiver, payload, attributes); _messageStatus[messageId] = CrossChainCallStatus.Requested; - emit MessagePosted( - messageId, sourceChain, sender, destinationChain, receiver, payload, msg.value, expandedAttributes - ); + emit MessagePosted(messageId, sourceChain, sender, destinationChain, receiver, payload, msg.value, attributes); return messageId; } @@ -188,41 +187,54 @@ abstract contract RRC7755Outbox is RRC7755Base { /// @custom:reverts If finality delay seconds have not passed since the request was fulfilled on destination chain /// @custom:reverts If the reward attribute is not found in the attributes array /// - /// @param destinationChain The chain identifier of the destination chain - /// @param receiver The account address of the receiver - /// @param payload The encoded calls array - /// @param expandedAttributes The attributes to be included in the message - /// @param proof A proof that cryptographically verifies that `fulfillmentInfo` does, indeed, exist in - /// storage on the destination chain - /// @param payTo The address the Filler wants to receive the reward + /// @param destinationChain The chain identifier of the destination chain + /// @param receiver The account address of the receiver + /// @param payload The encoded calls array + /// @param attributes The attributes to be included in the message + /// @param proof A proof that cryptographically verifies that `fulfillmentInfo` does, indeed, exist in + /// storage on the destination chain + /// @param payTo The address the Filler wants to receive the reward function claimReward( bytes32 destinationChain, bytes32 receiver, bytes calldata payload, - bytes[] calldata expandedAttributes, + bytes[] calldata attributes, bytes calldata proof, address payTo ) external { - bytes32 messageId; - { - bytes32 sender = address(this).addressToBytes32(); - bytes32 sourceChain = bytes32(block.chainid); - bool isUserOp = _isUserOp(expandedAttributes); - messageId = - getRequestId(sourceChain, sender, destinationChain, receiver, payload, expandedAttributes, isUserOp); - } - - _checkValidStatus({requestHash: messageId, expectedStatus: CrossChainCallStatus.Requested}); + bytes32 sender = address(this).addressToBytes32(); + bytes32 sourceChain = bytes32(block.chainid); + bytes32 messageId = getRequestId(sourceChain, sender, destinationChain, receiver, payload, attributes); bytes memory storageKey = abi.encode(keccak256(abi.encodePacked(messageId, _VERIFIER_STORAGE_LOCATION))); - _validateProof(storageKey, receiver.bytes32ToAddress(), expandedAttributes, proof); + _validateProof(storageKey, receiver.bytes32ToAddress(), attributes, proof); - _messageStatus[messageId] = CrossChainCallStatus.Completed; + (bytes32 rewardAsset, uint256 rewardAmount) = _getReward(attributes); - (bytes32 rewardAsset, uint256 rewardAmount) = _getReward(expandedAttributes); - _sendReward(payTo, rewardAsset, rewardAmount); + _processClaim(messageId, payTo, rewardAsset, rewardAmount); + } - emit CrossChainCallCompleted(messageId, msg.sender); + /// @notice To be called by a Filler that successfully submitted a cross chain user operation to the destination chain + /// + /// @custom:reverts If the request is not in the `CrossChainCallStatus.Requested` state + /// @custom:reverts If storage proof invalid + /// @custom:reverts If finality delay seconds have not passed since the request was fulfilled on destination chain + /// @custom:reverts If the reward attribute is not found in the attributes array + /// + /// @param userOp The ERC-4337 User Operation + /// @param proof A proof that cryptographically verifies that `fulfillmentInfo` does, indeed, exist in + /// storage on the destination chain + /// @param payTo The address the Filler wants to receive the reward + function claimReward(PackedUserOperation calldata userOp, bytes calldata proof, address payTo) external { + bytes32 messageId = userOp.hash(); + + bytes memory storageKey = abi.encode(keccak256(abi.encodePacked(messageId, _VERIFIER_STORAGE_LOCATION))); + address inbox = address(bytes20(userOp.paymasterAndData[:20])); + bytes[] memory attributes = getUserOpAttributes(userOp); + (bytes32 rewardAsset, uint256 rewardAmount) = + this.innerValidateProofAndGetReward(storageKey, inbox, attributes, proof); + + _processClaim(messageId, payTo, rewardAsset, rewardAmount); } /// @notice Cancels a pending request that has expired @@ -234,43 +246,43 @@ abstract contract RRC7755Outbox is RRC7755Base { /// @custom:reverts If the current block timestamp is less than the expiry timestamp plus the cancel delay seconds /// @custom:reverts If the reward attribute is not found in the attributes array /// - /// @param destinationChain The CAIP-2 chain identifier of the destination chain - /// @param receiver The account address of the receiver - /// @param payload The encoded calls to be included in the request - /// @param expandedAttributes The attributes to be included in the message + /// @param destinationChain The CAIP-2 chain identifier of the destination chain + /// @param receiver The account address of the receiver + /// @param payload The encoded calls to be included in the request + /// @param attributes The attributes to be included in the message function cancelMessage( bytes32 destinationChain, bytes32 receiver, bytes calldata payload, - bytes[] calldata expandedAttributes + bytes[] calldata attributes ) external { bytes32 sender = address(this).addressToBytes32(); bytes32 sourceChain = bytes32(block.chainid); - bool isUserOp = _isUserOp(expandedAttributes); - bytes32 messageId = - getRequestId(sourceChain, sender, destinationChain, receiver, payload, expandedAttributes, isUserOp); - - _checkValidStatus({requestHash: messageId, expectedStatus: CrossChainCallStatus.Requested}); + bytes32 messageId = getRequestId(sourceChain, sender, destinationChain, receiver, payload, attributes); (bytes32 requester, uint256 expiry, bytes32 rewardAsset, uint256 rewardAmount) = - _getRequesterAndExpiryAndReward(expandedAttributes); + getRequesterAndExpiryAndReward(attributes); - if (msg.sender.addressToBytes32() != requester) { - revert InvalidCaller({caller: msg.sender, expectedCaller: requester.bytes32ToAddress()}); - } - if (block.timestamp < expiry + CANCEL_DELAY_SECONDS) { - revert CannotCancelRequestBeforeExpiry({ - currentTimestamp: block.timestamp, - expiry: expiry + CANCEL_DELAY_SECONDS - }); - } + _processCancellation(messageId, requester, expiry, rewardAsset, rewardAmount); + } - _messageStatus[messageId] = CrossChainCallStatus.Canceled; + /// @notice Cancels a pending user op request that has expired + /// + /// @custom:reverts If the request is not in the `CrossChainCallStatus.Requested` state + /// @custom:reverts If the requester attribute is not found in the attributes array + /// @custom:reverts If the delay attribute is not found in the attributes array + /// @custom:reverts If `msg.sender` is not the requester defined by the requester attribute + /// @custom:reverts If the current block timestamp is less than the expiry timestamp plus the cancel delay seconds + /// + /// @param userOp The ERC-4337 User Operation + function cancelUserOp(PackedUserOperation calldata userOp) external { + bytes32 messageId = userOp.hash(); + bytes[] memory attributes = getUserOpAttributes(userOp); - // Return the stored reward back to the original requester - _sendReward(requester.bytes32ToAddress(), rewardAsset, rewardAmount); + (bytes32 requester, uint256 expiry, bytes32 rewardAsset, uint256 rewardAmount) = + this.getRequesterAndExpiryAndReward(attributes); - emit CrossChainCallCanceled(messageId); + _processCancellation(messageId, requester, expiry, rewardAsset, rewardAmount); } /// @notice Returns the cross chain call request status for a hashed request @@ -291,6 +303,86 @@ abstract contract RRC7755Outbox is RRC7755Base { return selector == _REWARD_ATTRIBUTE_SELECTOR || selector == _DELAY_ATTRIBUTE_SELECTOR; } + /// @notice This is only to be called by this contract during a `sendMessage` call + /// + /// @custom:reverts If the caller is not this contract + /// + /// @param attributes The attributes to be processed + /// @param requester The address of the requester + /// @param value The value of the message + function processAttributes(bytes[] calldata attributes, address requester, uint256 value) public { + if (msg.sender != address(this)) { + revert InvalidCaller({caller: msg.sender, expectedCaller: address(this)}); + } + + bool[4] memory attributeProcessed = [false, false, false, false]; + + for (uint256 i; i < attributes.length; i++) { + bytes4 attributeSelector = bytes4(attributes[i]); + + if (attributeSelector == _REWARD_ATTRIBUTE_SELECTOR && !attributeProcessed[0]) { + _handleRewardAttribute(attributes[i], requester, value); + attributeProcessed[0] = true; + } else if (attributeSelector == _DELAY_ATTRIBUTE_SELECTOR && !attributeProcessed[1]) { + _handleDelayAttribute(attributes[i]); + attributeProcessed[1] = true; + } else if (attributeSelector == _NONCE_ATTRIBUTE_SELECTOR && !attributeProcessed[2]) { + // confirm passed in nonce == _getNextNonce() + if (abi.decode(attributes[i][4:], (uint256)) != _getNextNonce()) { + revert InvalidNonce(); + } + attributeProcessed[2] = true; + } else if (attributeSelector == _REQUESTER_ATTRIBUTE_SELECTOR && !attributeProcessed[3]) { + // confirm passed in requester == msg.sender + if (abi.decode(attributes[i][4:], (bytes32)) != requester.addressToBytes32()) { + revert InvalidRequester(); + } + attributeProcessed[3] = true; + } else if (!_isOptionalAttribute(attributeSelector)) { + revert UnsupportedAttribute(attributeSelector); + } + } + + if (!attributeProcessed[0]) { + revert MissingRequiredAttribute(_REWARD_ATTRIBUTE_SELECTOR); + } + + if (!attributeProcessed[1]) { + revert MissingRequiredAttribute(_DELAY_ATTRIBUTE_SELECTOR); + } + + if (!attributeProcessed[2]) { + revert MissingRequiredAttribute(_NONCE_ATTRIBUTE_SELECTOR); + } + + if (!attributeProcessed[3]) { + revert MissingRequiredAttribute(_REQUESTER_ATTRIBUTE_SELECTOR); + } + } + + /// @notice Validates storage proofs and verifies fill + /// + /// @custom:reverts If storage proof invalid + /// @custom:reverts If fillInfo not found at inboxContractStorageKey on crossChainCall.verifyingContract + /// @custom:reverts If fillInfo.timestamp is less than crossChainCall.finalityDelaySeconds from current destination + /// chain block timestamp + /// + /// @param inboxContractStorageKey The storage location of the data to verify on the destination chain + /// `RRC7755Inbox` contract + /// @param inbox The address of the `RRC7755Inbox` contract + /// @param attributes The attributes to be included in the message + /// @param proofData The proof to validate + function innerValidateProofAndGetReward( + bytes memory inboxContractStorageKey, + address inbox, + bytes[] calldata attributes, + bytes calldata proofData + ) public view returns (bytes32, uint256) { + _validateProof(inboxContractStorageKey, inbox, attributes, proofData); + (bytes32 rewardAsset, uint256 rewardAmount) = _getReward(attributes); + return (rewardAsset, rewardAmount); + } + /// @notice Returns the keccak256 hash of a message request or the user op hash if the request is an ERC-4337 User /// Operation /// @@ -300,7 +392,6 @@ abstract contract RRC7755Outbox is RRC7755Base { /// @param receiver The account address of the receiver /// @param payload The messages to be included in the request /// @param attributes The attributes to be included in the message - /// @param isUserOp Whether the request is an ERC-4337 User Operation /// /// @return _ The keccak256 hash of the message request function getRequestId( @@ -309,12 +400,11 @@ abstract contract RRC7755Outbox is RRC7755Base { bytes32 destinationChain, bytes32 receiver, bytes calldata payload, - bytes[] calldata attributes, - bool isUserOp - ) public view returns (bytes32) { - return isUserOp + bytes[] calldata attributes + ) public view override returns (bytes32) { + return attributes.length == 0 ? this.getUserOpHash(abi.decode(payload, (PackedUserOperation))) - : getRequestId(sourceChain, sender, destinationChain, receiver, payload, attributes); + : super.getRequestId(sourceChain, sender, destinationChain, receiver, payload, attributes); } /// @notice Returns the hash of an ERC-4337 User Operation @@ -326,6 +416,45 @@ abstract contract RRC7755Outbox is RRC7755Base { return userOp.hash(); } + /// @notice Returns the requester, expiry, reward asset, and reward amount from the attributes array + /// + /// @param attributes The attributes to be included in the message + /// + /// @return _ The requester, expiry, reward asset, and reward amount + function getRequesterAndExpiryAndReward(bytes[] calldata attributes) + public + pure + returns (bytes32, uint256, bytes32, uint256) + { + bytes32 requester; + uint256 expiry; + bytes32 rewardAsset; + uint256 rewardAmount; + + for (uint256 i; i < attributes.length; i++) { + if (bytes4(attributes[i]) == _REQUESTER_ATTRIBUTE_SELECTOR) { + requester = abi.decode(attributes[i][4:], (bytes32)); + } else if (bytes4(attributes[i]) == _DELAY_ATTRIBUTE_SELECTOR) { + (, expiry) = abi.decode(attributes[i][4:], (uint256, uint256)); + } else if (bytes4(attributes[i]) == _REWARD_ATTRIBUTE_SELECTOR) { + (rewardAsset, rewardAmount) = abi.decode(attributes[i][4:], (bytes32, uint256)); + } + } + + return (requester, expiry, rewardAsset, rewardAmount); + } + + /// @notice Returns the attributes for an ERC-4337 User Operation + /// + /// @param userOp The ERC-4337 User Operation + /// + /// @return _ The attributes for the ERC-4337 User Operation + function getUserOpAttributes(PackedUserOperation calldata userOp) public pure returns (bytes[] memory) { + (,,, bytes[] memory userOpAttributes) = + abi.decode(userOp.paymasterAndData[52:], (address, uint256, address, bytes[])); + return userOpAttributes; + } + /// @notice Validates storage proofs and verifies fill /// /// @custom:reverts If storage proof invalid @@ -363,72 +492,22 @@ abstract contract RRC7755Outbox is RRC7755Base { return fulfillmentInfo; } - function _processAttributes(bytes[] calldata attributes) private returns (bytes[] memory, bool) { - if (attributes.length < _EXPECTED_ATTRIBUTE_LENGTH) { - revert InvalidAttributeLength(_EXPECTED_ATTRIBUTE_LENGTH, attributes.length); - } - - bytes[] memory adjustedAttributes = new bytes[](attributes.length + 2); - bool[2] memory attributeProcessed = [false, false]; - bool isUserOp; - - for (uint256 i; i < attributes.length; i++) { - bytes4 attributeSelector = bytes4(attributes[i]); - - if (attributeSelector == _REWARD_ATTRIBUTE_SELECTOR && !attributeProcessed[0]) { - _handleRewardAttribute(attributes[i]); - attributeProcessed[0] = true; - } else if (attributeSelector == _DELAY_ATTRIBUTE_SELECTOR && !attributeProcessed[1]) { - _handleDelayAttribute(attributes[i]); - attributeProcessed[1] = true; - } else if (!_isOptionalAttribute(attributeSelector)) { - revert UnsupportedAttribute(attributeSelector); - } - - if (attributeSelector == _USER_OP_ATTRIBUTE_SELECTOR) { - isUserOp = abi.decode(attributes[i][4:], (bool)); - } - - adjustedAttributes[i] = attributes[i]; - } - - if (!attributeProcessed[0]) { - revert MissingRequiredAttribute(_REWARD_ATTRIBUTE_SELECTOR); - } - - if (!attributeProcessed[1]) { - revert MissingRequiredAttribute(_DELAY_ATTRIBUTE_SELECTOR); - } - - adjustedAttributes[attributes.length] = abi.encodeWithSelector(_NONCE_ATTRIBUTE_SELECTOR, _getNextNonce()); - adjustedAttributes[attributes.length + 1] = - abi.encodeWithSelector(_REQUESTER_ATTRIBUTE_SELECTOR, msg.sender.addressToBytes32()); - - return (adjustedAttributes, isUserOp); - } - - function _isUserOp(bytes[] calldata attributes) private pure returns (bool) { - for (uint256 i; i < attributes.length; i++) { - if (bytes4(attributes[i]) == _USER_OP_ATTRIBUTE_SELECTOR) { - return abi.decode(attributes[i][4:], (bool)); - } - } - - return false; + function _isOptionalAttribute(bytes4 selector) internal pure virtual returns (bool) { + return selector == _PRECHECK_ATTRIBUTE_SELECTOR || selector == _L2_ORACLE_ATTRIBUTE_SELECTOR; } - function _handleRewardAttribute(bytes calldata attribute) private { + function _handleRewardAttribute(bytes calldata attribute, address requester, uint256 value) private { (bytes32 rewardAsset, uint256 rewardAmount) = abi.decode(attribute[4:], (bytes32, uint256)); bool usingNativeCurrency = rewardAsset == _NATIVE_ASSET; uint256 expectedValue = usingNativeCurrency ? rewardAmount : 0; - if (msg.value != expectedValue) { - revert InvalidValue(expectedValue, msg.value); + if (value != expectedValue) { + revert InvalidValue(expectedValue, value); } if (!usingNativeCurrency) { - rewardAsset.bytes32ToAddress().safeTransferFrom(msg.sender, address(this), rewardAmount); + rewardAsset.bytes32ToAddress().safeTransferFrom(requester, address(this), rewardAmount); } } @@ -440,6 +519,41 @@ abstract contract RRC7755Outbox is RRC7755Base { } } + function _processClaim(bytes32 messageId, address payTo, bytes32 rewardAsset, uint256 rewardAmount) private { + _checkValidStatus({requestHash: messageId, expectedStatus: CrossChainCallStatus.Requested}); + _messageStatus[messageId] = CrossChainCallStatus.Completed; + _sendReward(payTo, rewardAsset, rewardAmount); + + emit CrossChainCallCompleted(messageId, msg.sender); + } + + function _processCancellation( + bytes32 messageId, + bytes32 requester, + uint256 expiry, + bytes32 rewardAsset, + uint256 rewardAmount + ) private { + _checkValidStatus({requestHash: messageId, expectedStatus: CrossChainCallStatus.Requested}); + + if (msg.sender.addressToBytes32() != requester) { + revert InvalidCaller({caller: msg.sender, expectedCaller: requester.bytes32ToAddress()}); + } + if (block.timestamp < expiry + CANCEL_DELAY_SECONDS) { + revert CannotCancelRequestBeforeExpiry({ + currentTimestamp: block.timestamp, + expiry: expiry + CANCEL_DELAY_SECONDS + }); + } + + _messageStatus[messageId] = CrossChainCallStatus.Canceled; + + // Return the stored reward back to the original requester + _sendReward(requester.bytes32ToAddress(), rewardAsset, rewardAmount); + + emit CrossChainCallCanceled(messageId); + } + function _sendReward(address to, bytes32 rewardAsset, uint256 rewardAmount) private { if (rewardAsset == _NATIVE_ASSET) { to.safeTransferETH(rewardAmount); @@ -464,9 +578,9 @@ abstract contract RRC7755Outbox is RRC7755Base { } } - function _isOptionalAttribute(bytes4 selector) internal pure virtual returns (bool) { - return selector == _PRECHECK_ATTRIBUTE_SELECTOR || selector == _L2_ORACLE_ATTRIBUTE_SELECTOR - || selector == _USER_OP_ATTRIBUTE_SELECTOR; + function _getUserOpAttributes(bytes calldata payload) private view returns (bytes[] memory) { + PackedUserOperation memory userOp = abi.decode(payload, (PackedUserOperation)); + return this.getUserOpAttributes(userOp); } function _getReward(bytes[] calldata attributes) private pure returns (bytes32, uint256) { @@ -481,27 +595,4 @@ abstract contract RRC7755Outbox is RRC7755Base { return (rewardAsset, rewardAmount); } - - function _getRequesterAndExpiryAndReward(bytes[] calldata attributes) - private - pure - returns (bytes32, uint256, bytes32, uint256) - { - bytes32 requester; - uint256 expiry; - bytes32 rewardAsset; - uint256 rewardAmount; - - for (uint256 i; i < attributes.length; i++) { - if (bytes4(attributes[i]) == _REQUESTER_ATTRIBUTE_SELECTOR) { - requester = abi.decode(attributes[i][4:], (bytes32)); - } else if (bytes4(attributes[i]) == _DELAY_ATTRIBUTE_SELECTOR) { - (, expiry) = abi.decode(attributes[i][4:], (uint256, uint256)); - } else if (bytes4(attributes[i]) == _REWARD_ATTRIBUTE_SELECTOR) { - (rewardAsset, rewardAmount) = abi.decode(attributes[i][4:], (bytes32, uint256)); - } - } - - return (requester, expiry, rewardAsset, rewardAmount); - } } diff --git a/contracts/test/RRC7755Inbox.t.sol b/contracts/test/RRC7755Inbox.t.sol index 3e98110..f766384 100644 --- a/contracts/test/RRC7755Inbox.t.sol +++ b/contracts/test/RRC7755Inbox.t.sol @@ -157,16 +157,15 @@ contract RRC7755InboxTest is BaseTest { bytes32 sourceChain = bytes32(block.chainid); bytes32 sender = address(this).addressToBytes32(); bytes memory payload = abi.encode(new Call[](0)); - bytes[] memory attributes = new bytes[](isPrecheck ? 6 : 5); + bytes[] memory attributes = new bytes[](isPrecheck ? 5 : 4); attributes[0] = abi.encodeWithSelector(_REWARD_ATTRIBUTE_SELECTOR, bytes32(0), uint256(0)); attributes[1] = abi.encodeWithSelector(_DELAY_ATTRIBUTE_SELECTOR, 10, block.timestamp + 11); attributes[2] = abi.encodeWithSelector(_NONCE_ATTRIBUTE_SELECTOR, 1); attributes[3] = abi.encodeWithSelector(_REQUESTER_ATTRIBUTE_SELECTOR, ALICE.addressToBytes32()); - attributes[4] = abi.encodeWithSelector(_USER_OP_ATTRIBUTE_SELECTOR, isUserOp); if (isPrecheck) { - attributes[5] = abi.encodeWithSelector(_PRECHECK_ATTRIBUTE_SELECTOR, address(precheck)); + attributes[4] = abi.encodeWithSelector(_PRECHECK_ATTRIBUTE_SELECTOR, address(precheck)); } return TestMessage({ @@ -176,7 +175,7 @@ contract RRC7755InboxTest is BaseTest { sourceChain: sourceChain, sender: sender, payload: payload, - attributes: attributes + attributes: isUserOp ? new bytes[](0) : attributes }); } diff --git a/contracts/test/RRC7755Outbox.t.sol b/contracts/test/RRC7755Outbox.t.sol index 37b75e9..a5d7f34 100644 --- a/contracts/test/RRC7755Outbox.t.sol +++ b/contracts/test/RRC7755Outbox.t.sol @@ -11,14 +11,17 @@ import {BaseTest} from "./BaseTest.t.sol"; contract RRC7755OutboxTest is BaseTest { using GlobalTypes for address; + using GlobalTypes for bytes; struct TestMessage { bytes32 sourceChain; bytes32 destinationChain; bytes32 sender; bytes32 receiver; + PackedUserOperation userOp; bytes payload; bytes[] attributes; + bytes[] userOpAttributes; } MockOutbox outbox; @@ -48,23 +51,21 @@ contract RRC7755OutboxTest is BaseTest { TestMessage memory m = _initMessage(rewardAmount / 2, false); bytes32 messageId = _deriveMessageId(m); - bytes[] memory adjustedAttributes = _getAdjustedAttributes(m); - vm.prank(ALICE); vm.expectEmit(true, false, false, true); emit MessagePosted( - messageId, m.sourceChain, m.sender, m.destinationChain, m.receiver, m.payload, 0, adjustedAttributes + messageId, m.sourceChain, m.sender, m.destinationChain, m.receiver, m.payload, 0, m.attributes ); outbox.sendMessage(m.destinationChain, m.receiver, m.payload, m.attributes); - adjustedAttributes[2] = abi.encodeWithSelector(_NONCE_ATTRIBUTE_SELECTOR, 2); + m.attributes[2] = abi.encodeWithSelector(_NONCE_ATTRIBUTE_SELECTOR, 2); messageId = - outbox.getRequestId(m.sourceChain, m.sender, m.destinationChain, m.receiver, m.payload, adjustedAttributes); + outbox.getRequestId(m.sourceChain, m.sender, m.destinationChain, m.receiver, m.payload, m.attributes); vm.prank(ALICE); vm.expectEmit(true, false, false, true); emit MessagePosted( - messageId, m.sourceChain, m.sender, m.destinationChain, m.receiver, m.payload, 0, adjustedAttributes + messageId, m.sourceChain, m.sender, m.destinationChain, m.receiver, m.payload, 0, m.attributes ); outbox.sendMessage(m.destinationChain, m.receiver, m.payload, m.attributes); } @@ -78,15 +79,6 @@ contract RRC7755OutboxTest is BaseTest { outbox.sendMessage(m.destinationChain, m.receiver, m.payload, m.attributes); } - function test_sendMessage_reverts_ifMissingAttributes(uint256 rewardAmount) external fundAlice(rewardAmount) { - vm.assume(rewardAmount > 0); - TestMessage memory m = _initMessage(rewardAmount, false); - - vm.prank(ALICE); - vm.expectRevert(abi.encodeWithSelector(RRC7755Outbox.InvalidAttributeLength.selector, 2, 0)); - outbox.sendMessage(m.destinationChain, m.receiver, m.payload, new bytes[](0)); - } - function test_sendMessage_reverts_ifNativeCurrencyIncludedUnnecessarily(uint256 rewardAmount) external fundAlice(rewardAmount) @@ -190,6 +182,49 @@ contract RRC7755OutboxTest is BaseTest { outbox.sendMessage(m.destinationChain, m.receiver, m.payload, m.attributes); } + function test_sendMessage_reverts_ifIncorrectNonce(uint256 rewardAmount) external fundAlice(rewardAmount) { + TestMessage memory m = _initMessage(rewardAmount, false); + m.attributes[2] = abi.encodeWithSelector(_NONCE_ATTRIBUTE_SELECTOR, 1000); + + vm.prank(ALICE); + vm.expectRevert(RRC7755Outbox.InvalidNonce.selector); + outbox.sendMessage(m.destinationChain, m.receiver, m.payload, m.attributes); + } + + function test_sendMessage_reverts_ifMissingNonceAttribute(uint256 rewardAmount) external fundAlice(rewardAmount) { + TestMessage memory m = _initMessage(rewardAmount, false); + m.attributes[2] = abi.encodeWithSelector(_PRECHECK_ATTRIBUTE_SELECTOR); + + vm.prank(ALICE); + vm.expectRevert( + abi.encodeWithSelector(RRC7755Outbox.MissingRequiredAttribute.selector, _NONCE_ATTRIBUTE_SELECTOR) + ); + outbox.sendMessage(m.destinationChain, m.receiver, m.payload, m.attributes); + } + + function test_sendMessage_reverts_ifIncorrectRequester(uint256 rewardAmount) external fundAlice(rewardAmount) { + TestMessage memory m = _initMessage(rewardAmount, false); + m.attributes[3] = abi.encodeWithSelector(_REQUESTER_ATTRIBUTE_SELECTOR, FILLER.addressToBytes32()); + + vm.prank(ALICE); + vm.expectRevert(RRC7755Outbox.InvalidRequester.selector); + outbox.sendMessage(m.destinationChain, m.receiver, m.payload, m.attributes); + } + + function test_sendMessage_reverts_ifMissingRequesterAttribute(uint256 rewardAmount) + external + fundAlice(rewardAmount) + { + TestMessage memory m = _initMessage(rewardAmount, false); + m.attributes[3] = abi.encodeWithSelector(_PRECHECK_ATTRIBUTE_SELECTOR); + + vm.prank(ALICE); + vm.expectRevert( + abi.encodeWithSelector(RRC7755Outbox.MissingRequiredAttribute.selector, _REQUESTER_ATTRIBUTE_SELECTOR) + ); + outbox.sendMessage(m.destinationChain, m.receiver, m.payload, m.attributes); + } + function test_sendMessage_setStatusToRequested_nativeAssetReward(uint256 rewardAmount) external fundAlice(rewardAmount) @@ -210,7 +245,7 @@ contract RRC7755OutboxTest is BaseTest { vm.prank(ALICE); vm.expectEmit(true, false, false, true); emit MessagePosted( - messageId, m.sourceChain, m.sender, m.destinationChain, m.receiver, m.payload, 0, _getAdjustedAttributes(m) + messageId, m.sourceChain, m.sender, m.destinationChain, m.receiver, m.payload, 0, m.attributes ); outbox.sendMessage(m.destinationChain, m.receiver, m.payload, m.attributes); } @@ -241,6 +276,11 @@ contract RRC7755OutboxTest is BaseTest { assertEq(contractBalAfter - contractBalBefore, rewardAmount); } + function test_processAttributes_reverts_ifInvalidCaller() external { + vm.expectRevert(abi.encodeWithSelector(RRC7755Outbox.InvalidCaller.selector, address(this), address(outbox))); + outbox.processAttributes(new bytes[](0), address(outbox), 0); + } + function test_claimReward_reverts_requestDoesNotExist(uint256 rewardAmount) external fundAlice(rewardAmount) { TestMessage memory m = _initMessage(rewardAmount, false); bytes memory storageProofData = abi.encode(true); @@ -261,9 +301,7 @@ contract RRC7755OutboxTest is BaseTest { bytes memory storageProofData = abi.encode(true); vm.prank(FILLER); - outbox.claimReward( - m.destinationChain, m.receiver, m.payload, _getAdjustedAttributes(m), storageProofData, FILLER - ); + outbox.claimReward(m.destinationChain, m.receiver, m.payload, m.attributes, storageProofData, FILLER); vm.prank(FILLER); vm.expectRevert( @@ -273,18 +311,16 @@ contract RRC7755OutboxTest is BaseTest { RRC7755Outbox.CrossChainCallStatus.Completed ) ); - outbox.claimReward( - m.destinationChain, m.receiver, m.payload, _getAdjustedAttributes(m), storageProofData, FILLER - ); + outbox.claimReward(m.destinationChain, m.receiver, m.payload, m.attributes, storageProofData, FILLER); } function test_claimReward_reverts_requestCanceled(uint256 rewardAmount) external fundAlice(rewardAmount) { TestMessage memory m = _submitRequest(rewardAmount); bytes memory storageProofData = abi.encode(true); - vm.warp(this.extractExpiry(_getAdjustedAttributes(m)) + outbox.CANCEL_DELAY_SECONDS()); + vm.warp(this.extractExpiry(m.attributes) + outbox.CANCEL_DELAY_SECONDS()); vm.prank(ALICE); - outbox.cancelMessage(m.destinationChain, m.receiver, m.payload, _getAdjustedAttributes(m)); + outbox.cancelMessage(m.destinationChain, m.receiver, m.payload, m.attributes); vm.prank(FILLER); vm.expectRevert( @@ -294,9 +330,7 @@ contract RRC7755OutboxTest is BaseTest { RRC7755Outbox.CrossChainCallStatus.Canceled ) ); - outbox.claimReward( - m.destinationChain, m.receiver, m.payload, _getAdjustedAttributes(m), storageProofData, FILLER - ); + outbox.claimReward(m.destinationChain, m.receiver, m.payload, m.attributes, storageProofData, FILLER); } function test_claimReward_emitsEvent(uint256 rewardAmount) external fundAlice(rewardAmount) { @@ -306,9 +340,7 @@ contract RRC7755OutboxTest is BaseTest { vm.expectEmit(true, false, false, true); emit CrossChainCallCompleted(_deriveMessageId(m), FILLER); vm.prank(FILLER); - outbox.claimReward( - m.destinationChain, m.receiver, m.payload, _getAdjustedAttributes(m), storageProofData, FILLER - ); + outbox.claimReward(m.destinationChain, m.receiver, m.payload, m.attributes, storageProofData, FILLER); } function test_claimReward_storesCompletedStatus_pendingState(uint256 rewardAmount) @@ -319,9 +351,7 @@ contract RRC7755OutboxTest is BaseTest { bytes memory storageProofData = abi.encode(true); vm.prank(FILLER); - outbox.claimReward( - m.destinationChain, m.receiver, m.payload, _getAdjustedAttributes(m), storageProofData, FILLER - ); + outbox.claimReward(m.destinationChain, m.receiver, m.payload, m.attributes, storageProofData, FILLER); RRC7755Outbox.CrossChainCallStatus status = outbox.getMessageStatus(_deriveMessageId(m)); assert(status == RRC7755Outbox.CrossChainCallStatus.Completed); @@ -335,11 +365,9 @@ contract RRC7755OutboxTest is BaseTest { bytes memory storageProofData = abi.encode(true); vm.prank(FILLER); - outbox.claimReward( - m.destinationChain, m.receiver, m.payload, _getAdjustedAttributes(m), storageProofData, FILLER - ); + outbox.claimReward(m.userOp, storageProofData, FILLER); - bytes32 messageId = outbox.getUserOpHash(abi.decode(m.payload, (PackedUserOperation))); + bytes32 messageId = outbox.getUserOpHash(m.userOp); RRC7755Outbox.CrossChainCallStatus status = outbox.getMessageStatus(messageId); assert(status == RRC7755Outbox.CrossChainCallStatus.Completed); } @@ -354,9 +382,7 @@ contract RRC7755OutboxTest is BaseTest { uint256 fillerBalBefore = FILLER.balance; vm.prank(FILLER); - outbox.claimReward( - m.destinationChain, m.receiver, m.payload, _getAdjustedAttributes(m), storageProofData, FILLER - ); + outbox.claimReward(m.destinationChain, m.receiver, m.payload, m.attributes, storageProofData, FILLER); uint256 fillerBalAfter = FILLER.balance; @@ -376,9 +402,7 @@ contract RRC7755OutboxTest is BaseTest { uint256 contractBalBefore = address(outbox).balance; vm.prank(FILLER); - outbox.claimReward( - m.destinationChain, m.receiver, m.payload, _getAdjustedAttributes(m), storageProofData, FILLER - ); + outbox.claimReward(m.destinationChain, m.receiver, m.payload, m.attributes, storageProofData, FILLER); uint256 contractBalAfter = address(outbox).balance; @@ -392,9 +416,7 @@ contract RRC7755OutboxTest is BaseTest { uint256 fillerBalBefore = mockErc20.balanceOf(FILLER); vm.prank(FILLER); - outbox.claimReward( - m.destinationChain, m.receiver, m.payload, _getAdjustedAttributes(m), storageProofData, FILLER - ); + outbox.claimReward(m.destinationChain, m.receiver, m.payload, m.attributes, storageProofData, FILLER); uint256 fillerBalAfter = mockErc20.balanceOf(FILLER); @@ -408,9 +430,7 @@ contract RRC7755OutboxTest is BaseTest { uint256 contractBalBefore = mockErc20.balanceOf(address(outbox)); vm.prank(FILLER); - outbox.claimReward( - m.destinationChain, m.receiver, m.payload, _getAdjustedAttributes(m), storageProofData, FILLER - ); + outbox.claimReward(m.destinationChain, m.receiver, m.payload, m.attributes, storageProofData, FILLER); uint256 contractBalAfter = mockErc20.balanceOf(address(outbox)); @@ -427,15 +447,15 @@ contract RRC7755OutboxTest is BaseTest { RRC7755Outbox.CrossChainCallStatus.None ) ); - outbox.cancelMessage(m.destinationChain, m.receiver, m.payload, _getAdjustedAttributes(m)); + outbox.cancelMessage(m.destinationChain, m.receiver, m.payload, m.attributes); } function test_cancelMessage_reverts_requestAlreadyCanceled(uint256 rewardAmount) external fundAlice(rewardAmount) { TestMessage memory m = _submitRequest(rewardAmount); - vm.warp(this.extractExpiry(_getAdjustedAttributes(m)) + outbox.CANCEL_DELAY_SECONDS()); + vm.warp(this.extractExpiry(m.attributes) + outbox.CANCEL_DELAY_SECONDS()); vm.prank(ALICE); - outbox.cancelMessage(m.destinationChain, m.receiver, m.payload, _getAdjustedAttributes(m)); + outbox.cancelMessage(m.destinationChain, m.receiver, m.payload, m.attributes); vm.expectRevert( abi.encodeWithSelector( @@ -444,7 +464,7 @@ contract RRC7755OutboxTest is BaseTest { RRC7755Outbox.CrossChainCallStatus.Canceled ) ); - outbox.cancelMessage(m.destinationChain, m.receiver, m.payload, _getAdjustedAttributes(m)); + outbox.cancelMessage(m.destinationChain, m.receiver, m.payload, m.attributes); } function test_cancelMessage_reverts_requestAlreadyCompleted(uint256 rewardAmount) @@ -455,9 +475,7 @@ contract RRC7755OutboxTest is BaseTest { bytes memory storageProofData = abi.encode(true); vm.prank(FILLER); - outbox.claimReward( - m.destinationChain, m.receiver, m.payload, _getAdjustedAttributes(m), storageProofData, FILLER - ); + outbox.claimReward(m.destinationChain, m.receiver, m.payload, m.attributes, storageProofData, FILLER); vm.expectRevert( abi.encodeWithSelector( @@ -466,7 +484,7 @@ contract RRC7755OutboxTest is BaseTest { RRC7755Outbox.CrossChainCallStatus.Completed ) ); - outbox.cancelMessage(m.destinationChain, m.receiver, m.payload, _getAdjustedAttributes(m)); + outbox.cancelMessage(m.destinationChain, m.receiver, m.payload, m.attributes); } function test_cancelMessage_reverts_invalidCaller(uint256 rewardAmount) external fundAlice(rewardAmount) { @@ -474,40 +492,40 @@ contract RRC7755OutboxTest is BaseTest { vm.prank(FILLER); vm.expectRevert(abi.encodeWithSelector(RRC7755Outbox.InvalidCaller.selector, FILLER, ALICE)); - outbox.cancelMessage(m.destinationChain, m.receiver, m.payload, _getAdjustedAttributes(m)); + outbox.cancelMessage(m.destinationChain, m.receiver, m.payload, m.attributes); } function test_cancelMessage_reverts_requestStillActive(uint256 rewardAmount) external fundAlice(rewardAmount) { TestMessage memory m = _submitRequest(rewardAmount); uint256 cancelDelaySeconds = outbox.CANCEL_DELAY_SECONDS(); - vm.warp(this.extractExpiry(_getAdjustedAttributes(m)) + cancelDelaySeconds - 1); + vm.warp(this.extractExpiry(m.attributes) + cancelDelaySeconds - 1); vm.expectRevert( abi.encodeWithSelector( RRC7755Outbox.CannotCancelRequestBeforeExpiry.selector, block.timestamp, - this.extractExpiry(_getAdjustedAttributes(m)) + cancelDelaySeconds + this.extractExpiry(m.attributes) + cancelDelaySeconds ) ); vm.prank(ALICE); - outbox.cancelMessage(m.destinationChain, m.receiver, m.payload, _getAdjustedAttributes(m)); + outbox.cancelMessage(m.destinationChain, m.receiver, m.payload, m.attributes); } function test_cancelMessage_reverts_ifInvalidCaller(uint256 rewardAmount) external fundAlice(rewardAmount) { TestMessage memory m = _submitRequest(rewardAmount); - vm.warp(this.extractExpiry(_getAdjustedAttributes(m)) + outbox.CANCEL_DELAY_SECONDS()); + vm.warp(this.extractExpiry(m.attributes) + outbox.CANCEL_DELAY_SECONDS()); vm.prank(FILLER); vm.expectRevert(abi.encodeWithSelector(RRC7755Outbox.InvalidCaller.selector, FILLER, ALICE)); - outbox.cancelMessage(m.destinationChain, m.receiver, m.payload, _getAdjustedAttributes(m)); + outbox.cancelMessage(m.destinationChain, m.receiver, m.payload, m.attributes); } function test_cancelMessage_setsStatusAsCanceled(uint256 rewardAmount) external fundAlice(rewardAmount) { TestMessage memory m = _submitRequest(rewardAmount); - vm.warp(this.extractExpiry(_getAdjustedAttributes(m)) + outbox.CANCEL_DELAY_SECONDS()); + vm.warp(this.extractExpiry(m.attributes) + outbox.CANCEL_DELAY_SECONDS()); vm.prank(ALICE); - outbox.cancelMessage(m.destinationChain, m.receiver, m.payload, _getAdjustedAttributes(m)); + outbox.cancelMessage(m.destinationChain, m.receiver, m.payload, m.attributes); RRC7755Outbox.CrossChainCallStatus status = outbox.getMessageStatus(_deriveMessageId(m)); assert(status == RRC7755Outbox.CrossChainCallStatus.Canceled); @@ -516,11 +534,11 @@ contract RRC7755OutboxTest is BaseTest { function test_cancelMessage_setsStatusAsCanceled_userOp(uint256 rewardAmount) external fundAlice(rewardAmount) { TestMessage memory m = _submitUserOp(rewardAmount); - vm.warp(this.extractExpiry(_getAdjustedAttributes(m)) + outbox.CANCEL_DELAY_SECONDS()); + vm.warp(this.extractExpiry(m.userOpAttributes) + outbox.CANCEL_DELAY_SECONDS()); vm.prank(ALICE); - outbox.cancelMessage(m.destinationChain, m.receiver, m.payload, _getAdjustedAttributes(m)); + outbox.cancelUserOp(m.userOp); - bytes32 messageId = outbox.getUserOpHash(abi.decode(m.payload, (PackedUserOperation))); + bytes32 messageId = outbox.getUserOpHash(m.userOp); RRC7755Outbox.CrossChainCallStatus status = outbox.getMessageStatus(messageId); assert(status == RRC7755Outbox.CrossChainCallStatus.Canceled); } @@ -528,11 +546,11 @@ contract RRC7755OutboxTest is BaseTest { function test_cancelMessage_emitsCanceledEvent(uint256 rewardAmount) external fundAlice(rewardAmount) { TestMessage memory m = _submitRequest(rewardAmount); - vm.warp(this.extractExpiry(_getAdjustedAttributes(m)) + outbox.CANCEL_DELAY_SECONDS()); + vm.warp(this.extractExpiry(m.attributes) + outbox.CANCEL_DELAY_SECONDS()); vm.expectEmit(true, false, false, false); emit CrossChainCallCanceled(_deriveMessageId(m)); vm.prank(ALICE); - outbox.cancelMessage(m.destinationChain, m.receiver, m.payload, _getAdjustedAttributes(m)); + outbox.cancelMessage(m.destinationChain, m.receiver, m.payload, m.attributes); } function test_cancelMessage_returnsNativeCurrencyToRequester(uint256 rewardAmount) @@ -546,9 +564,9 @@ contract RRC7755OutboxTest is BaseTest { uint256 aliceBalBefore = ALICE.balance; - vm.warp(this.extractExpiry(_getAdjustedAttributes(m)) + outbox.CANCEL_DELAY_SECONDS()); + vm.warp(this.extractExpiry(m.attributes) + outbox.CANCEL_DELAY_SECONDS()); vm.prank(ALICE); - outbox.cancelMessage(m.destinationChain, m.receiver, m.payload, _getAdjustedAttributes(m)); + outbox.cancelMessage(m.destinationChain, m.receiver, m.payload, m.attributes); uint256 aliceBalAfter = ALICE.balance; @@ -566,9 +584,9 @@ contract RRC7755OutboxTest is BaseTest { uint256 contractBalBefore = address(outbox).balance; - vm.warp(this.extractExpiry(_getAdjustedAttributes(m)) + outbox.CANCEL_DELAY_SECONDS()); + vm.warp(this.extractExpiry(m.attributes) + outbox.CANCEL_DELAY_SECONDS()); vm.prank(ALICE); - outbox.cancelMessage(m.destinationChain, m.receiver, m.payload, _getAdjustedAttributes(m)); + outbox.cancelMessage(m.destinationChain, m.receiver, m.payload, m.attributes); uint256 contractBalAfter = address(outbox).balance; @@ -580,9 +598,9 @@ contract RRC7755OutboxTest is BaseTest { uint256 aliceBalBefore = mockErc20.balanceOf(ALICE); - vm.warp(this.extractExpiry(_getAdjustedAttributes(m)) + outbox.CANCEL_DELAY_SECONDS()); + vm.warp(this.extractExpiry(m.attributes) + outbox.CANCEL_DELAY_SECONDS()); vm.prank(ALICE); - outbox.cancelMessage(m.destinationChain, m.receiver, m.payload, _getAdjustedAttributes(m)); + outbox.cancelMessage(m.destinationChain, m.receiver, m.payload, m.attributes); uint256 aliceBalAfter = mockErc20.balanceOf(ALICE); @@ -594,9 +612,9 @@ contract RRC7755OutboxTest is BaseTest { uint256 contractBalBefore = mockErc20.balanceOf(address(outbox)); - vm.warp(this.extractExpiry(_getAdjustedAttributes(m)) + outbox.CANCEL_DELAY_SECONDS()); + vm.warp(this.extractExpiry(m.attributes) + outbox.CANCEL_DELAY_SECONDS()); vm.prank(ALICE); - outbox.cancelMessage(m.destinationChain, m.receiver, m.payload, _getAdjustedAttributes(m)); + outbox.cancelMessage(m.destinationChain, m.receiver, m.payload, m.attributes); uint256 contractBalAfter = mockErc20.balanceOf(address(outbox)); @@ -636,7 +654,7 @@ contract RRC7755OutboxTest is BaseTest { bytes32 sender = address(outbox).addressToBytes32(); Call[] memory calls = new Call[](1); calls[0] = Call({to: address(outbox).addressToBytes32(), data: "", value: 0}); - bytes[] memory attributes = new bytes[](2); + bytes[] memory attributes = new bytes[](4); if (isNativeAsset) { attributes[0] = abi.encodeWithSelector(_REWARD_ATTRIBUTE_SELECTOR, _NATIVE_ASSET, rewardAmount); @@ -646,20 +664,35 @@ contract RRC7755OutboxTest is BaseTest { } attributes = _setDelay(attributes, 10, block.timestamp + 11); + attributes[2] = abi.encodeWithSelector(_NONCE_ATTRIBUTE_SELECTOR, 1); + attributes[3] = abi.encodeWithSelector(_REQUESTER_ATTRIBUTE_SELECTOR, ALICE.addressToBytes32()); + + PackedUserOperation memory userOp; return TestMessage({ sourceChain: bytes32(block.chainid), destinationChain: destinationChain, sender: sender, receiver: sender, + userOp: userOp, payload: abi.encode(calls), - attributes: attributes + attributes: attributes, + userOpAttributes: new bytes[](0) }); } function _initUserOpMessage(uint256 rewardAmount) private view returns (TestMessage memory) { bytes32 destinationChain = bytes32(block.chainid); bytes32 sender = address(outbox).addressToBytes32(); + bytes[] memory attributes = new bytes[](4); + + attributes[0] = + abi.encodeWithSelector(_REWARD_ATTRIBUTE_SELECTOR, address(mockErc20).addressToBytes32(), rewardAmount); + + attributes = _setDelay(attributes, 10, block.timestamp + 11); + attributes[2] = abi.encodeWithSelector(_NONCE_ATTRIBUTE_SELECTOR, 1); + attributes[3] = abi.encodeWithSelector(_REQUESTER_ATTRIBUTE_SELECTOR, ALICE.addressToBytes32()); + PackedUserOperation memory userOp = PackedUserOperation({ sender: address(0), nonce: 1, @@ -668,24 +701,19 @@ contract RRC7755OutboxTest is BaseTest { accountGasLimits: 0, preVerificationGas: 0, gasFees: 0, - paymasterAndData: "", + paymasterAndData: _encodePaymasterAndData(address(outbox), attributes, ALICE), signature: "" }); - bytes[] memory attributes = new bytes[](3); - - attributes[0] = - abi.encodeWithSelector(_REWARD_ATTRIBUTE_SELECTOR, address(mockErc20).addressToBytes32(), rewardAmount); - - attributes = _setDelay(attributes, 10, block.timestamp + 11); - attributes[2] = abi.encodeWithSelector(_USER_OP_ATTRIBUTE_SELECTOR, true); return TestMessage({ sourceChain: bytes32(block.chainid), destinationChain: destinationChain, sender: sender, receiver: sender, + userOp: userOp, payload: abi.encode(userOp), - attributes: attributes + attributes: new bytes[](0), + userOpAttributes: attributes }); } @@ -713,19 +741,23 @@ contract RRC7755OutboxTest is BaseTest { } function _deriveMessageId(TestMessage memory m) private view returns (bytes32) { - bytes[] memory adjustedAttributes = _getAdjustedAttributes(m); - return - outbox.getRequestId(m.sourceChain, m.sender, m.destinationChain, m.receiver, m.payload, adjustedAttributes); + return outbox.getRequestId(m.sourceChain, m.sender, m.destinationChain, m.receiver, m.payload, m.attributes); } - function _getAdjustedAttributes(TestMessage memory m) private view returns (bytes[] memory) { - bytes[] memory adjustedAttributes = new bytes[](m.attributes.length + 2); - for (uint256 i = 0; i < m.attributes.length; i++) { - adjustedAttributes[i] = m.attributes[i]; - } - adjustedAttributes[m.attributes.length] = abi.encodeWithSelector(_NONCE_ATTRIBUTE_SELECTOR, 1); - adjustedAttributes[m.attributes.length + 1] = - abi.encodeWithSelector(_REQUESTER_ATTRIBUTE_SELECTOR, ALICE.addressToBytes32()); - return adjustedAttributes; + function _encodePaymasterAndData(address inbox, bytes[] memory attributes, address ethAddress) + private + pure + returns (bytes memory) + { + address precheck = address(0); + uint256 ethAmount = 0.0001 ether; + uint128 paymasterVerificationGasLimit = 100000; + uint128 paymasterPostOpGasLimit = 100000; + return abi.encodePacked( + inbox, + paymasterVerificationGasLimit, + paymasterPostOpGasLimit, + abi.encode(ethAddress, ethAmount, precheck, attributes) + ); } }