From c6d283e67dc8b88de503692b9b1b57214895a14f Mon Sep 17 00:00:00 2001 From: Igor Aleksanov Date: Thu, 24 Apr 2025 13:51:04 +0400 Subject: [PATCH 01/11] Basic implementation --- .../upgrade/SystemContractsProcessing.s.sol | 6 + .../contracts/erc4337/EntryPointV01.sol | 146 ++++++++++++++++++ .../contracts/interfaces/IEntryPoint.sol | 9 ++ system-contracts/scripts/constants.ts | 5 + 4 files changed, 166 insertions(+) create mode 100644 system-contracts/contracts/erc4337/EntryPointV01.sol create mode 100644 system-contracts/contracts/interfaces/IEntryPoint.sol diff --git a/l1-contracts/deploy-scripts/upgrade/SystemContractsProcessing.s.sol b/l1-contracts/deploy-scripts/upgrade/SystemContractsProcessing.s.sol index c0f12e6dbc..7fa772de22 100644 --- a/l1-contracts/deploy-scripts/upgrade/SystemContractsProcessing.s.sol +++ b/l1-contracts/deploy-scripts/upgrade/SystemContractsProcessing.s.sol @@ -253,6 +253,12 @@ library SystemContractsProcessing { lang: Language.Solidity, isPrecompile: false }); + systemContracts[31] = SystemContract({ + addr: 0x0000000000000000000000000000000000008016, + codeName: "EntryPoint", + lang: Language.Solidity, + isPrecompile: false + }); return systemContracts; } diff --git a/system-contracts/contracts/erc4337/EntryPointV01.sol b/system-contracts/contracts/erc4337/EntryPointV01.sol new file mode 100644 index 0000000000..ff96f74e50 --- /dev/null +++ b/system-contracts/contracts/erc4337/EntryPointV01.sol @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {IEntryPoint} from "../interfaces/IEntryPoint.sol"; +import {IBootloaderUtilities} from "../interfaces/IBootloaderUtilities.sol"; +import {IAccount} from "../interfaces/IAccount.sol"; +import {IAccountCodeStorage} from "../interfaces/IAccountCodeStorage.sol"; +import {INonceHolder} from "../interfaces/INonceHolder.sol"; +import {IContractDeployer} from "../interfaces/IContractDeployer.sol"; +import {ISystemContext} from "../interfaces/ISystemContext.sol"; +import {ContractDeployer} from "../ContractDeployer.sol"; +import {Transaction, TransactionHelper, EIP_712_TX_TYPE, LEGACY_TX_TYPE, EIP_2930_TX_TYPE, EIP_1559_TX_TYPE} from "../libraries/TransactionHelper.sol"; +import {BOOTLOADER_FORMAL_ADDRESS, SYSTEM_CONTEXT_CONTRACT, NONCE_HOLDER_SYSTEM_CONTRACT, ACCOUNT_CODE_STORAGE_SYSTEM_CONTRACT, DEPLOYER_SYSTEM_CONTRACT} from "../Constants.sol"; +import {SystemContractBase} from "../abstract/SystemContractBase.sol"; +import {SystemContext} from "../SystemContext.sol"; +import {RLPEncoder} from "../libraries/RLPEncoder.sol"; +import {EfficientCall} from "../libraries/EfficientCall.sol"; +import {UnsupportedTxType, InvalidSig, SigField, HashMismatch} from "../SystemContractErrors.sol"; + +import {SystemContractsCaller, CalldataForwardingMode} from "../libraries/SystemContractsCaller.sol"; + +/** + * @author Matter Labs + * @custom:security-contact security@matterlabs.dev + * @notice An EIP4337 EntryPoint contract implementation + * built on top of the native ZKsync Account Abstraction. + */ +contract EntryPointV01 is IEntryPoint, SystemContractBase { + using TransactionHelper for *; + + // TODO: `Transaction` should be replaced with EIP4337 `UserOperation` + function handleUserOps(Transaction[] calldata _transactions) external { + for (uint i = 0; i < _transactions.length; i++) { + _handleTransaction(_transactions[i]); + } + } + + function _handleTransaction(Transaction calldata tx) private { + bytes32 txHash = bytes32(0); // TODO: Should we calculate it for user? + bytes32 suggestedTxHash = bytes32(0); // TODO: Should we calculate it for user? + + _validateTransaction(txHash, suggestedTxHash, tx); + _payForTransaction(txHash, suggestedTxHash, tx); + + // TODO: here and below, we probably should not revert after the transaction payment; + // instead we should go to the next transaction. + _executeTransaction(txHash, suggestedTxHash, tx); + + // refund? + } + + function _validateTransaction(bytes32 _txHash, bytes32 _suggestedSignedHash, Transaction memory _tx) private { + address from = address(uint160(_tx.from)); + // check account type via ContractDeployer + IContractDeployer.AccountAbstractionVersion version = ContractDeployer(address(DEPLOYER_SYSTEM_CONTRACT)) + .extendedAccountVersion(from); + require(version == IContractDeployer.AccountAbstractionVersion.Version1, "Unsupported account version"); + + // check that nonce is available yet + NONCE_HOLDER_SYSTEM_CONTRACT.validateNonceUsage(from, _tx.nonce, false); + + // validate transaction + bytes memory returnData = this._performMimicCall( + uint32(gasleft()), // Should be value from the transaction? + BOOTLOADER_FORMAL_ADDRESS, + from, + abi.encodeCall(IAccount(from).validateTransaction, (_txHash, _suggestedSignedHash, _tx)) + ); + bytes4 magic = abi.decode(returnData, (bytes4)); + // We have to revert, since user didn't pay for the transaction just yet. + require(magic == IAccount.validateTransaction.selector, "Verification failed"); + + // check that nonce is not available anymore + NONCE_HOLDER_SYSTEM_CONTRACT.validateNonceUsage(from, _tx.nonce, true); + } + + function _payForTransaction(bytes32 _txHash, bytes32 _suggestedSignedHash, Transaction memory _tx) private { + address from = address(uint160(_tx.from)); + if (_tx.paymaster == 0) { + // No paymaster, pay directly + uint256 bootloaderBalanceBefore = address(BOOTLOADER_FORMAL_ADDRESS).balance; + this._performMimicCall( + uint32(gasleft()), // Should be value from the transaction? + BOOTLOADER_FORMAL_ADDRESS, + from, + abi.encodeCall(IAccount(from).payForTransaction, (_txHash, _suggestedSignedHash, _tx)) + ); + uint256 bootloaderBalanceAfter = address(BOOTLOADER_FORMAL_ADDRESS).balance; + require(bootloaderBalanceAfter > bootloaderBalanceBefore, "Transaction payment failed"); + require( + bootloaderBalanceAfter - bootloaderBalanceBefore >= _tx.gasLimit * _tx.maxFeePerGas, + "Transaction payment amount mismatch" + ); + // TODO: should we send back excessive funds like bootloader does? + } else { + // Pay through the paymaster + revert("Not implemented yet"); + } + } + + function _executeTransaction(bytes32 _txHash, bytes32 _suggestedSignedHash, Transaction memory _tx) private { + address from = address(uint160(_tx.from)); + require(_tx.factoryDeps.length == 0, "Factory deps cannot be sent through the EntryPoint contract"); + + // set tx.origin + bool isEOA = ACCOUNT_CODE_STORAGE_SYSTEM_CONTRACT.getRawCodeHash(from) == 0; + address txOrigin = address(0); + if (isEOA) { + txOrigin = from; + } + this._performMimicCall( + uint32(gasleft()), // Should be value from the transaction? + BOOTLOADER_FORMAL_ADDRESS, + address(SYSTEM_CONTEXT_CONTRACT), + abi.encodeCall(SystemContext.setTxOrigin, (txOrigin)) + ); + + // execute transaction + // TODO: do we need to do it through MsgValueSimulator? + this._performMimicCall( + uint32(gasleft()), // Should be value from the transaction? + BOOTLOADER_FORMAL_ADDRESS, + from, + abi.encodeCall(IAccount(from).executeTransaction, (_txHash, _suggestedSignedHash, _tx)) + ); + } + + // Needed to convert `memory` to `calldata` + function _performMimicCall( + uint32 _gas, + address _whoToMimic, + address _to, + bytes calldata _data + ) external onlyCallFrom(address(this)) returns (bytes memory returnData) { + return + EfficientCall.mimicCall( + _gas, + _to, + _data, + _whoToMimic, + false, + true // isSystem TODO <- is it required? + ); + } +} diff --git a/system-contracts/contracts/interfaces/IEntryPoint.sol b/system-contracts/contracts/interfaces/IEntryPoint.sol new file mode 100644 index 0000000000..ecca908692 --- /dev/null +++ b/system-contracts/contracts/interfaces/IEntryPoint.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT + +pragma solidity 0.8.24; + +import {Transaction} from "../libraries/TransactionHelper.sol"; + +interface IEntryPoint { + function handleUserOps(Transaction[] calldata _transactions) external; +} diff --git a/system-contracts/scripts/constants.ts b/system-contracts/scripts/constants.ts index 0827b2df1a..134b4a18da 100644 --- a/system-contracts/scripts/constants.ts +++ b/system-contracts/scripts/constants.ts @@ -166,6 +166,11 @@ export const SYSTEM_CONTRACTS: ISystemContracts = { codeName: "PubdataChunkPublisher", lang: Language.Solidity, }, + entryPoint: { + address: "0x0000000000000000000000000000000000008016", + codeName: "EntryPoint", + lang: Language.Solidity, + }, create2Factory: { // This is explicitly a non-system-contract address. // We do not use the same address as create2 factories on EVM, since From 6b40b7accdc971e6efbea8d4bcd86f0383abe4c8 Mon Sep 17 00:00:00 2001 From: Igor Aleksanov Date: Fri, 25 Apr 2025 15:31:08 +0400 Subject: [PATCH 02/11] Align EntryPoint interface with L1 --- .../contracts/erc4337/EntryPointV01.sol | 34 +++++++++++++++---- .../contracts/interfaces/IEntryPoint.sol | 17 +++++++++- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/system-contracts/contracts/erc4337/EntryPointV01.sol b/system-contracts/contracts/erc4337/EntryPointV01.sol index ff96f74e50..204a1fbd24 100644 --- a/system-contracts/contracts/erc4337/EntryPointV01.sol +++ b/system-contracts/contracts/erc4337/EntryPointV01.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.24; -import {IEntryPoint} from "../interfaces/IEntryPoint.sol"; +import {IEntryPoint, PackedUserOperation} from "../interfaces/IEntryPoint.sol"; import {IBootloaderUtilities} from "../interfaces/IBootloaderUtilities.sol"; import {IAccount} from "../interfaces/IAccount.sol"; import {IAccountCodeStorage} from "../interfaces/IAccountCodeStorage.sol"; @@ -29,14 +29,36 @@ import {SystemContractsCaller, CalldataForwardingMode} from "../libraries/System contract EntryPointV01 is IEntryPoint, SystemContractBase { using TransactionHelper for *; - // TODO: `Transaction` should be replaced with EIP4337 `UserOperation` - function handleUserOps(Transaction[] calldata _transactions) external { - for (uint i = 0; i < _transactions.length; i++) { - _handleTransaction(_transactions[i]); + function handleUserOps(PackedUserOperation[] calldata _ops) external { + for (uint i = 0; i < _ops.length; i++) { + PackedUserOperation memory op = _ops[i]; + Transaction memory tx = abi.decode(op.callData, (Transaction)); + + // Alignment checks. + // These are important in case data will be indexed: we want to make sure that + // "wrapped" fields are aligned with the original ones. + require(op.sender == address(uint160(tx.from)), "Sender mismatch"); + require(op.nonce == tx.nonce, "Nonce mismatch"); + require(op.initCode.length == 0, "Init code not supported"); + require(uint256(op.accountGasLimits) >> 128 == 0, "Verification gas limit must be 0"); + require(uint256(op.accountGasLimits) == tx.gasLimit, "Call gas limit mismatch"); + require(uint256(op.gasFees) >> 128 == tx.maxPriorityFeePerGas, "Max priority fee per gas mismatch"); + require(uint256(uint128(uint256(op.gasFees))) == tx.maxFeePerGas, "Max fee per gas mismatch"); + + require(op.preVerificationGas == 0, "Pre-verification gas limit must be 0"); + if (op.paymasterAndData.length > 0) { + // Decode address and params + (address paymaster, bytes memory paymasterInput) = abi.decode(op.paymasterAndData, (address, bytes)); // TODO: is that correct? + require(paymaster == address(uint160(tx.paymaster)), "Paymaster mismatch"); + require(keccak256(paymasterInput) == keccak256(tx.paymasterInput), "Paymaster input mismatch"); + } + require(keccak256(op.signature) == keccak256(tx.signature), "Signature mismatch"); + + _handleTransaction(tx); } } - function _handleTransaction(Transaction calldata tx) private { + function _handleTransaction(Transaction memory tx) private { bytes32 txHash = bytes32(0); // TODO: Should we calculate it for user? bytes32 suggestedTxHash = bytes32(0); // TODO: Should we calculate it for user? diff --git a/system-contracts/contracts/interfaces/IEntryPoint.sol b/system-contracts/contracts/interfaces/IEntryPoint.sol index ecca908692..01c1e9aa02 100644 --- a/system-contracts/contracts/interfaces/IEntryPoint.sol +++ b/system-contracts/contracts/interfaces/IEntryPoint.sol @@ -4,6 +4,21 @@ pragma solidity 0.8.24; import {Transaction} from "../libraries/TransactionHelper.sol"; +struct PackedUserOperation { + address sender; + uint256 nonce; + bytes initCode; + bytes callData; + /// @dev concatenation of verificationGasLimit (16 bytes) and callGasLimit (16 bytes) + bytes32 accountGasLimits; + uint256 preVerificationGas; + /// @dev concatenation of maxPriorityFeePerGas (16 bytes) and maxFeePerGas (16 bytes) + bytes32 gasFees; + /// @dev concatenation of paymaster fields (or empty) + bytes paymasterAndData; + bytes signature; +} + interface IEntryPoint { - function handleUserOps(Transaction[] calldata _transactions) external; + function handleUserOps(PackedUserOperation[] calldata _ops) external; } From 7fbb437c8921b93412e765a4a04d887283ae51d0 Mon Sep 17 00:00:00 2001 From: Igor Aleksanov Date: Tue, 29 Apr 2025 15:30:31 +0400 Subject: [PATCH 03/11] 7702 early draft --- system-contracts/bootloader/bootloader.yul | 78 +++++++++++++--- .../contracts/AccountCodeStorage.sol | 92 ++++++++++++++++++- .../interfaces/IAccountCodeStorage.sol | 2 + .../scripts/preprocess-bootloader.ts | 3 +- 4 files changed, 158 insertions(+), 17 deletions(-) diff --git a/system-contracts/bootloader/bootloader.yul b/system-contracts/bootloader/bootloader.yul index 0b2861f8f8..c7f5bdb600 100644 --- a/system-contracts/bootloader/bootloader.yul +++ b/system-contracts/bootloader/bootloader.yul @@ -694,6 +694,37 @@ object "Bootloader" { ret := mload(0) } + /// @notice Returns whether the account is EOA (even if it's EIP-7702 delegated). + /// @param addr The address of the account to check. + function isEOA(addr) -> ret { + mstore(0, {{RIGHT_PADDED_IS_ACCOUNT_EOA_SELECTOR}}) + mstore(4, addr) + let success := staticcall( + gas(), + ACCOUNT_CODE_STORAGE_ADDR(), + 0, + 36, + 0, + 32 + ) + + // In case the call to the account code storage fails, + // it most likely means that the caller did not provide enough gas for + // the call. + // In case the caller is certain that the amount of gas provided is enough, i.e. + // (`assertSuccess` = true), then we should panic. + if iszero(success) { + // Most likely not enough gas provided, revert the current frame. + nearCallPanic() + } + + ret := mload(0) + } + + function processDelegations(txDataOffset) -> ret { + // TODO: call AccountCodeStorage + } + /// @dev Calculates the canonical hash of the L1->L2 transaction that will be /// sent to L1 as a message to the L1 contract that a certain operation has been processed. function getCanonicalL1TxHash(txDataOffset) -> ret { @@ -2152,16 +2183,6 @@ object "Bootloader" { } } - /// @dev Checks whether an address is an EOA (i.e. has not code deployed on it) - /// @param addr The address to check - function isEOA(addr) -> ret { - ret := 0 - - if gt(addr, MAX_SYSTEM_CONTRACT_ADDR()) { - ret := iszero(getRawCodeHash(addr, false)) - } - } - /// @dev Calls the `payForTransaction` method of an account function accountPayForTx(account, txDataOffset) -> success { success := callAccountMethod({{PAY_FOR_TX_SELECTOR}}, account, txDataOffset) @@ -3105,11 +3126,6 @@ object "Bootloader" { /// @dev This function validates only L2 transactions, since the integrity of the L1->L2 /// transactions is enforced by the L1 smart contracts. function validateTypedTxStructure(innerTxDataOffset) { - /// Some common checks for all transactions. - let reservedDynamicLength := getReservedDynamicBytesLength(innerTxDataOffset) - if gt(reservedDynamicLength, 0) { - assertionError("non-empty reservedDynamic") - } let txType := getTxType(innerTxDataOffset) switch txType case 0 { @@ -3139,6 +3155,7 @@ object "Bootloader" { assertEq(getReserved3(innerTxDataOffset), 0, "reserved3 non zero") assertEq(getFactoryDepsBytesLength(innerTxDataOffset), 0, "factory deps non zero") assertEq(getPaymasterInputBytesLength(innerTxDataOffset), 0, "paymasterInput non zero") + assertEq(getReservedDynamicBytesLength(innerTxDataOffset), 0, "reservedDynamic non zero") } case 1 { let maxFeePerGas := getMaxFeePerGas(innerTxDataOffset) @@ -3165,6 +3182,7 @@ object "Bootloader" { assertEq(getReserved3(innerTxDataOffset), 0, "reserved3 non zero") assertEq(getFactoryDepsBytesLength(innerTxDataOffset), 0, "factory deps non zero") assertEq(getPaymasterInputBytesLength(innerTxDataOffset), 0, "paymasterInput non zero") + assertEq(getReservedDynamicBytesLength(innerTxDataOffset), 0, "reservedDynamic non zero") } case 2 { assertEq(lte(getGasPerPubdataByteLimit(innerTxDataOffset), MAX_L2_GAS_PER_PUBDATA()), 1, "Gas per pubdata is wrong") @@ -3188,6 +3206,35 @@ object "Bootloader" { assertEq(getReserved3(innerTxDataOffset), 0, "reserved3 non zero") assertEq(getFactoryDepsBytesLength(innerTxDataOffset), 0, "factory deps non zero") assertEq(getPaymasterInputBytesLength(innerTxDataOffset), 0, "paymasterInput non zero") + assertEq(getReservedDynamicBytesLength(innerTxDataOffset), 0, "reservedDynamic non zero") + } + case 4 { + assertEq(lte(getGasPerPubdataByteLimit(innerTxDataOffset), MAX_L2_GAS_PER_PUBDATA()), 1, "Gas per pubdata is wrong") + assertEq(getPaymaster(innerTxDataOffset), 0, "paymaster non zero") + + + + let from := getFrom(innerTxDataOffset) + let iseoa := isEOA(from) + assertEq(iseoa, true, "Only EIP-712 can use non-EOA") + + + + + assertEq(gt(getFrom(innerTxDataOffset), MAX_SYSTEM_CONTRACT_ADDR()), 1, "from in kernel space") + + + assertEq(getReserved0(innerTxDataOffset), 0, "reserved0 non zero") + // reserved1 used as marker that tx doesn't have field "to" + // however, for EIP7702, transactions without "to" are not allowed. + assertEq(getReserved1(innerTxDataOffset), 0, "reserved1 non zero") + assertEq(getReserved2(innerTxDataOffset), 0, "reserved2 non zero") + assertEq(getReserved3(innerTxDataOffset), 0, "reserved3 non zero") + assertEq(getFactoryDepsBytesLength(innerTxDataOffset), 0, "factory deps non zero") + assertEq(getPaymasterInputBytesLength(innerTxDataOffset), 0, "paymasterInput non zero") + + // For EIP7702, we use `reservedDynamic` to pass encoded authorization list data. + assertEq(gt(getReservedDynamicBytesLength(innerTxDataOffset), 0), 1, "reservedDynamic is zero for EIP7702") } case 113 { let paymaster := getPaymaster(innerTxDataOffset) @@ -3205,6 +3252,7 @@ object "Bootloader" { // reserved1 used as marker that tx doesn't have field "to" assertEq(getReserved2(innerTxDataOffset), 0, "reserved2 non zero") assertEq(getReserved3(innerTxDataOffset), 0, "reserved3 non zero") + assertEq(getReservedDynamicBytesLength(innerTxDataOffset), 0, "reservedDynamic non zero") } case 254 { // Upgrade transaction, no need to validate as it is validated on L1. diff --git a/system-contracts/contracts/AccountCodeStorage.sol b/system-contracts/contracts/AccountCodeStorage.sol index c977685c06..bd195951c9 100644 --- a/system-contracts/contracts/AccountCodeStorage.sol +++ b/system-contracts/contracts/AccountCodeStorage.sol @@ -3,10 +3,25 @@ pragma solidity 0.8.24; import {IAccountCodeStorage} from "./interfaces/IAccountCodeStorage.sol"; +import {SystemContractBase} from "./abstract/SystemContractBase.sol"; +import {Transaction} from "./libraries/TransactionHelper.sol"; +import {RLPEncoder} from "./libraries/RLPEncoder.sol"; import {Utils} from "./libraries/Utils.sol"; import {DEPLOYER_SYSTEM_CONTRACT, NONCE_HOLDER_SYSTEM_CONTRACT, CURRENT_MAX_PRECOMPILE_ADDRESS, EVM_HASHES_STORAGE} from "./Constants.sol"; import {Unauthorized, InvalidCodeHash, CodeHashReason} from "./SystemContractErrors.sol"; +/// @notice EIP-7702 authorization list item +/// @dev Authorization list items are passed from the transaction +/// through the bootloader. +struct AuthorizationListItem { + uint256 chainId; + uint256 nonce; + address addr; + uint256 yParity; + uint256 r; + uint256 s; +} + /** * @author Matter Labs * @custom:security-contact security@matterlabs.dev @@ -20,9 +35,15 @@ import {Unauthorized, InvalidCodeHash, CodeHashReason} from "./SystemContractErr * were published on L1 as calldata. This contract trusts the ContractDeployer and the KnownCodesStorage * system contracts to enforce the invariants mentioned above. */ -contract AccountCodeStorage is IAccountCodeStorage { +contract AccountCodeStorage is IAccountCodeStorage, SystemContractBase { bytes32 private constant EMPTY_STRING_KECCAK = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470; + /// @notice Information about EIP-7702 delegated EOAs. + /// @dev Delegated EOAs have code hash set to the code hash of the delegation address contract. + /// So, in order to check whether an account is an EOA, checking for the code hash is not enough. + /// We need to also check whether the account is a delegated EOA. + mapping(address => bool) private delegatedEOAs; + modifier onlyDeployer() { if (msg.sender != address(DEPLOYER_SYSTEM_CONTRACT)) { revert Unauthorized(msg.sender); @@ -153,4 +174,73 @@ contract AccountCodeStorage is IAccountCodeStorage { bytes32 bytecodeHash = getRawCodeHash(_addr); return Utils.isCodeHashEVM(bytecodeHash); } + + /// @notice Method for detecting whether an address is an EOA + /// @dev Checks whether the account either has no code hash set or is a delegated EOA. + function isAccountEOA(address _addr) external view override returns (bool) { + return delegatedEOAs[_addr] || getRawCodeHash(_addr) == 0x00; + } + + function processDelegations(AuthorizationListItem[] calldata authorizationList) external onlyCallFromBootloader { + for (uint256 i = 0; i < authorizationList.length; i++) { + // Per EIP7702 rules, if any check for the tuple item fails, + // we must move on to the next item in the list. + AuthorizationListItem calldata item = authorizationList[i]; + + // Verify the chain ID is 0 or the ID of the current chain. + if (item.chainId != 0 || item.chainId != block.chainid) { + continue; + } + + // Verify the nonce is less than 2**64 - 1. + if (item.nonce >= 0xFFFFFFFFFFFFFFFF) { + continue; + } + + // Calculate EIP7702 magic: + // msg = keccak(MAGIC || rlp([chain_id, address, nonce])) + bytes memory chainIdEncoded = RLPEncoder.encodeUint256(item.chainId); + bytes memory addressEncoded = RLPEncoder.encodeAddress(item.addr); + bytes memory nonceEncoded = RLPEncoder.encodeUint256(item.nonce); + bytes memory listLenEncoded = RLPEncoder.encodeListLen( + uint64(chainIdEncoded.length + addressEncoded.length + nonceEncoded.length) + ); + bytes32 message = keccak256( + bytes.concat(bytes1(0x05), listLenEncoded, chainIdEncoded, addressEncoded, nonceEncoded) + ); + + // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature + // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines + // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most + // signatures from current libraries generate a unique signature with an s-value in the lower half order. + // + // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value + // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or + // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept + // these malleable signatures as well. + if (uint256(item.s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) { + continue; + } + + address authority = ecrecover(message, uint8(item.yParity + 27), bytes32(item.r), bytes32(item.s)); + emit Log("Authority"); + emit LogAddr(authority); + + // ZKsync has native account abstraction, so we only allow delegation for EOAs. + if (!this.isAccountEOA(authority)) { + continue; + } + + if (item.nonce != NONCE_HOLDER_SYSTEM_CONTRACT.getRawNonce(item.addr)) { + emit Log("Nonce mismatch"); + continue; + } + delegatedEOAs[item.addr] = true; + // TODO: set code hash + // TODO: increment nonce + } + } } + +event Log(string); +event LogAddr(address); diff --git a/system-contracts/contracts/interfaces/IAccountCodeStorage.sol b/system-contracts/contracts/interfaces/IAccountCodeStorage.sol index 7bd24cc758..19b032a9e0 100644 --- a/system-contracts/contracts/interfaces/IAccountCodeStorage.sol +++ b/system-contracts/contracts/interfaces/IAccountCodeStorage.sol @@ -16,4 +16,6 @@ interface IAccountCodeStorage { function getCodeSize(uint256 _input) external view returns (uint256 codeSize); function isAccountEVM(address _addr) external view returns (bool); + + function isAccountEOA(address _addr) external view returns (bool); } diff --git a/system-contracts/scripts/preprocess-bootloader.ts b/system-contracts/scripts/preprocess-bootloader.ts index 29454dcd27..52b36aa4bd 100644 --- a/system-contracts/scripts/preprocess-bootloader.ts +++ b/system-contracts/scripts/preprocess-bootloader.ts @@ -67,7 +67,8 @@ const params = { VALIDATE_TX_SELECTOR: getSelector("IAccount", "validateTransaction"), EXECUTE_TX_SELECTOR: getSelector("DefaultAccount", "executeTransaction"), RIGHT_PADDED_GET_ACCOUNT_VERSION_SELECTOR: getPaddedSelector("ContractDeployer", "extendedAccountVersion"), - RIGHT_PADDED_GET_RAW_CODE_HASH_SELECTOR: getPaddedSelector("AccountCodeStorage", "getRawCodeHash"), + RIGHT_PADDED_IS_ACCOUNT_EOA_SELECTOR: getPaddedSelector("AccountCodeStorage", "isAccountEOA"), + RIGHT_PADDED_PROCESS_DELEGATIONS_SELECTOR: getPaddedSelector("AccountCodeStorage", "processDelegations"), PAY_FOR_TX_SELECTOR: getSelector("DefaultAccount", "payForTransaction"), PRE_PAYMASTER_SELECTOR: getSelector("DefaultAccount", "prepareForPaymaster"), VALIDATE_AND_PAY_PAYMASTER: getSelector("IPaymaster", "validateAndPayForPaymasterTransaction"), From 5a256d9bf1820abb938a68f41ca86700de93f996 Mon Sep 17 00:00:00 2001 From: Igor Aleksanov Date: Wed, 30 Apr 2025 10:15:56 +0400 Subject: [PATCH 04/11] Invoke processDelegations --- system-contracts/bootloader/bootloader.yul | 48 ++++++- .../contracts/AccountCodeStorage.sol | 119 +++++++++--------- 2 files changed, 108 insertions(+), 59 deletions(-) diff --git a/system-contracts/bootloader/bootloader.yul b/system-contracts/bootloader/bootloader.yul index c7f5bdb600..bfa8369cf7 100644 --- a/system-contracts/bootloader/bootloader.yul +++ b/system-contracts/bootloader/bootloader.yul @@ -721,8 +721,50 @@ object "Bootloader" { ret := mload(0) } - function processDelegations(txDataOffset) -> ret { - // TODO: call AccountCodeStorage + /// @notice invokes the `processDelegations` method of the `AccountCodeStorage` contract. + /// @dev this method expects `reservedDynamic` to contain ABI-encoded `AuthorizationList` + /// @dev this method internally overwrites transaction data and restores it after the call. + /// This is done to avoid copying the data to a new memory location. + function processDelegations(innerTxDataOffset) { + // 1. Read delegation length + let ptr := getReservedDynamicPtr(innerTxDataOffset) + let length := mload(ptr) + + // Delegations are only processed if transaction is EIP7702 and + // authorization list is provided + let isEIP7702 := eq(getTxType(innerTxDataOffset), 4) + let isDelegationProvided := gt(length, 0) + // let shouldProcess := and(isEIP7702, isDelegationProvided) + + if and(isEIP7702, isDelegationProvided) { + // 2. Overwrite the delegation length word with right-padded selector + // This will work because `reservedDynamic` is `bytes`, so the first word + // is the length; but for us the contents are already ABI-encoded data. + mstore(ptr, {{RIGHT_PADDED_PROCESS_DELEGATIONS_SELECTOR}}) + + // 3. Call the method + let calldataOffset := add(ptr, 28) + let calldataLength := add(length, 4) + let success := call( + gas(), + ACCOUNT_CODE_STORAGE_ADDR(), + 0, + calldataOffset, + calldataLength, + 0, + 0 + ) + + // 4. Restore the length in memory + mstore(length, {{RIGHT_PADDED_PROCESS_DELEGATIONS_SELECTOR}}) + + // 5. Process the result + // If the transaction failed, either there was not enough gas or compression is malformed. + if iszero(success) { + debugLog("processing delegations failed", 0) + nearCallPanic() + } + } } /// @dev Calculates the canonical hash of the L1->L2 transaction that will be @@ -1496,6 +1538,8 @@ object "Bootloader" { setTxOrigin(BOOTLOADER_FORMAL_ADDR()) } + processDelegations(innerTxDataOffset) + success := executeL2Tx(txDataOffset, from) if isNotEnoughGasForPubdata( diff --git a/system-contracts/contracts/AccountCodeStorage.sol b/system-contracts/contracts/AccountCodeStorage.sol index bd195951c9..012593edda 100644 --- a/system-contracts/contracts/AccountCodeStorage.sol +++ b/system-contracts/contracts/AccountCodeStorage.sol @@ -182,65 +182,70 @@ contract AccountCodeStorage is IAccountCodeStorage, SystemContractBase { } function processDelegations(AuthorizationListItem[] calldata authorizationList) external onlyCallFromBootloader { - for (uint256 i = 0; i < authorizationList.length; i++) { - // Per EIP7702 rules, if any check for the tuple item fails, - // we must move on to the next item in the list. - AuthorizationListItem calldata item = authorizationList[i]; - - // Verify the chain ID is 0 or the ID of the current chain. - if (item.chainId != 0 || item.chainId != block.chainid) { - continue; - } - - // Verify the nonce is less than 2**64 - 1. - if (item.nonce >= 0xFFFFFFFFFFFFFFFF) { - continue; - } - - // Calculate EIP7702 magic: - // msg = keccak(MAGIC || rlp([chain_id, address, nonce])) - bytes memory chainIdEncoded = RLPEncoder.encodeUint256(item.chainId); - bytes memory addressEncoded = RLPEncoder.encodeAddress(item.addr); - bytes memory nonceEncoded = RLPEncoder.encodeUint256(item.nonce); - bytes memory listLenEncoded = RLPEncoder.encodeListLen( - uint64(chainIdEncoded.length + addressEncoded.length + nonceEncoded.length) - ); - bytes32 message = keccak256( - bytes.concat(bytes1(0x05), listLenEncoded, chainIdEncoded, addressEncoded, nonceEncoded) - ); - - // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature - // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines - // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most - // signatures from current libraries generate a unique signature with an s-value in the lower half order. - // - // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value - // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or - // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept - // these malleable signatures as well. - if (uint256(item.s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) { - continue; - } - - address authority = ecrecover(message, uint8(item.yParity + 27), bytes32(item.r), bytes32(item.s)); - emit Log("Authority"); - emit LogAddr(authority); - - // ZKsync has native account abstraction, so we only allow delegation for EOAs. - if (!this.isAccountEOA(authority)) { - continue; - } - - if (item.nonce != NONCE_HOLDER_SYSTEM_CONTRACT.getRawNonce(item.addr)) { - emit Log("Nonce mismatch"); - continue; - } - delegatedEOAs[item.addr] = true; - // TODO: set code hash - // TODO: increment nonce - } + emit Log("processDelegations"); + emit Log("Number of delegations"); + emit LogNumber(authorizationList.length); + + // for (uint256 i = 0; i < authorizationList.length; i++) { + // // Per EIP7702 rules, if any check for the tuple item fails, + // // we must move on to the next item in the list. + // AuthorizationListItem calldata item = authorizationList[i]; + + // // Verify the chain ID is 0 or the ID of the current chain. + // if (item.chainId != 0 || item.chainId != block.chainid) { + // continue; + // } + + // // Verify the nonce is less than 2**64 - 1. + // if (item.nonce >= 0xFFFFFFFFFFFFFFFF) { + // continue; + // } + + // // Calculate EIP7702 magic: + // // msg = keccak(MAGIC || rlp([chain_id, address, nonce])) + // bytes memory chainIdEncoded = RLPEncoder.encodeUint256(item.chainId); + // bytes memory addressEncoded = RLPEncoder.encodeAddress(item.addr); + // bytes memory nonceEncoded = RLPEncoder.encodeUint256(item.nonce); + // bytes memory listLenEncoded = RLPEncoder.encodeListLen( + // uint64(chainIdEncoded.length + addressEncoded.length + nonceEncoded.length) + // ); + // bytes32 message = keccak256( + // bytes.concat(bytes1(0x05), listLenEncoded, chainIdEncoded, addressEncoded, nonceEncoded) + // ); + + // // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature + // // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines + // // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most + // // signatures from current libraries generate a unique signature with an s-value in the lower half order. + // // + // // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value + // // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or + // // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept + // // these malleable signatures as well. + // if (uint256(item.s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) { + // continue; + // } + + // address authority = ecrecover(message, uint8(item.yParity + 27), bytes32(item.r), bytes32(item.s)); + // emit Log("Authority"); + // emit LogAddr(authority); + + // // ZKsync has native account abstraction, so we only allow delegation for EOAs. + // if (!this.isAccountEOA(authority)) { + // continue; + // } + + // if (item.nonce != NONCE_HOLDER_SYSTEM_CONTRACT.getRawNonce(item.addr)) { + // emit Log("Nonce mismatch"); + // continue; + // } + // delegatedEOAs[item.addr] = true; + // // TODO: set code hash + // // TODO: increment nonce + // } } } event Log(string); event LogAddr(address); +event LogNumber(uint256); From 94f5a9b3e2986d78f06d5a7be7a4bd111fb7f5e2 Mon Sep 17 00:00:00 2001 From: Igor Aleksanov Date: Wed, 30 Apr 2025 15:41:44 +0400 Subject: [PATCH 05/11] 7702 verification works --- system-contracts/bootloader/bootloader.yul | 11 +- .../contracts/AccountCodeStorage.sol | 14 +- .../contracts/BootloaderUtilities.sol | 146 +++++++++++++++++- .../contracts/libraries/TransactionHelper.sol | 132 ++++++++++++++++ .../scripts/preprocess-bootloader.ts | 3 +- 5 files changed, 287 insertions(+), 19 deletions(-) diff --git a/system-contracts/bootloader/bootloader.yul b/system-contracts/bootloader/bootloader.yul index bfa8369cf7..bce188d3dc 100644 --- a/system-contracts/bootloader/bootloader.yul +++ b/system-contracts/bootloader/bootloader.yul @@ -726,6 +726,7 @@ object "Bootloader" { /// @dev this method internally overwrites transaction data and restores it after the call. /// This is done to avoid copying the data to a new memory location. function processDelegations(innerTxDataOffset) { + debugLog("processDelegations", 0) // 1. Read delegation length let ptr := getReservedDynamicPtr(innerTxDataOffset) let length := mload(ptr) @@ -734,13 +735,14 @@ object "Bootloader" { // authorization list is provided let isEIP7702 := eq(getTxType(innerTxDataOffset), 4) let isDelegationProvided := gt(length, 0) - // let shouldProcess := and(isEIP7702, isDelegationProvided) + let shouldProcess := and(isEIP7702, isDelegationProvided) + debugLog("shouldProcessDelegations", shouldProcess) - if and(isEIP7702, isDelegationProvided) { + if shouldProcess { // 2. Overwrite the delegation length word with right-padded selector // This will work because `reservedDynamic` is `bytes`, so the first word // is the length; but for us the contents are already ABI-encoded data. - mstore(ptr, {{RIGHT_PADDED_PROCESS_DELEGATIONS_SELECTOR}}) + mstore(ptr, {{PROCESS_DELEGATIONS_SELECTOR}}) // 3. Call the method let calldataOffset := add(ptr, 28) @@ -756,7 +758,7 @@ object "Bootloader" { ) // 4. Restore the length in memory - mstore(length, {{RIGHT_PADDED_PROCESS_DELEGATIONS_SELECTOR}}) + mstore(ptr, length) // 5. Process the result // If the transaction failed, either there was not enough gas or compression is malformed. @@ -3171,6 +3173,7 @@ object "Bootloader" { /// transactions is enforced by the L1 smart contracts. function validateTypedTxStructure(innerTxDataOffset) { let txType := getTxType(innerTxDataOffset) + debugLog("txType", txType) switch txType case 0 { let maxFeePerGas := getMaxFeePerGas(innerTxDataOffset) diff --git a/system-contracts/contracts/AccountCodeStorage.sol b/system-contracts/contracts/AccountCodeStorage.sol index 012593edda..fa10f29605 100644 --- a/system-contracts/contracts/AccountCodeStorage.sol +++ b/system-contracts/contracts/AccountCodeStorage.sol @@ -4,24 +4,12 @@ pragma solidity 0.8.24; import {IAccountCodeStorage} from "./interfaces/IAccountCodeStorage.sol"; import {SystemContractBase} from "./abstract/SystemContractBase.sol"; -import {Transaction} from "./libraries/TransactionHelper.sol"; +import {Transaction, AuthorizationListItem} from "./libraries/TransactionHelper.sol"; import {RLPEncoder} from "./libraries/RLPEncoder.sol"; import {Utils} from "./libraries/Utils.sol"; import {DEPLOYER_SYSTEM_CONTRACT, NONCE_HOLDER_SYSTEM_CONTRACT, CURRENT_MAX_PRECOMPILE_ADDRESS, EVM_HASHES_STORAGE} from "./Constants.sol"; import {Unauthorized, InvalidCodeHash, CodeHashReason} from "./SystemContractErrors.sol"; -/// @notice EIP-7702 authorization list item -/// @dev Authorization list items are passed from the transaction -/// through the bootloader. -struct AuthorizationListItem { - uint256 chainId; - uint256 nonce; - address addr; - uint256 yParity; - uint256 r; - uint256 s; -} - /** * @author Matter Labs * @custom:security-contact security@matterlabs.dev diff --git a/system-contracts/contracts/BootloaderUtilities.sol b/system-contracts/contracts/BootloaderUtilities.sol index 554a79aba7..46a41ef13c 100644 --- a/system-contracts/contracts/BootloaderUtilities.sol +++ b/system-contracts/contracts/BootloaderUtilities.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.24; import {IBootloaderUtilities} from "./interfaces/IBootloaderUtilities.sol"; -import {Transaction, TransactionHelper, EIP_712_TX_TYPE, LEGACY_TX_TYPE, EIP_2930_TX_TYPE, EIP_1559_TX_TYPE} from "./libraries/TransactionHelper.sol"; +import {Transaction, TransactionHelper, AuthorizationListItem, EIP_712_TX_TYPE, LEGACY_TX_TYPE, EIP_2930_TX_TYPE, EIP_1559_TX_TYPE, EIP_7702_TX_TYPE} from "./libraries/TransactionHelper.sol"; import {RLPEncoder} from "./libraries/RLPEncoder.sol"; import {EfficientCall} from "./libraries/EfficientCall.sol"; import {UnsupportedTxType, InvalidSig, SigField} from "./SystemContractErrors.sol"; @@ -34,6 +34,8 @@ contract BootloaderUtilities is IBootloaderUtilities { txHash = encodeEIP1559TransactionHash(_transaction); } else if (_transaction.txType == EIP_2930_TX_TYPE) { txHash = encodeEIP2930TransactionHash(_transaction); + } else if (_transaction.txType == EIP_7702_TX_TYPE) { + txHash = encodeEIP7702TransactionHash(_transaction); } else { revert UnsupportedTxType(_transaction.txType); } @@ -339,4 +341,146 @@ contract BootloaderUtilities is IBootloaderUtilities { ) ); } + + /// @notice Encode hash of the EIP7702 transaction type. + /// @return txHash The hash of the transaction. + function encodeEIP7702TransactionHash(Transaction calldata _transaction) internal view returns (bytes32) { + // Transaction hash of EIP1559 transactions is encoded the following way: + // H(0x04 || RLP(chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, destination, value, data, access_list, authorization_list, v, r, s)) + // + // Note, that on ZKsync access lists are not supported and should always be empty. + // However, the authorization list is supported and taken into account. + + // Encode all fixed-length params to avoid "stack too deep error" + bytes memory encodedFixedLengthParams; + { + bytes memory encodedChainId = RLPEncoder.encodeUint256(block.chainid); + bytes memory encodedNonce = RLPEncoder.encodeUint256(_transaction.nonce); + bytes memory encodedMaxPriorityFeePerGas = RLPEncoder.encodeUint256(_transaction.maxPriorityFeePerGas); + bytes memory encodedMaxFeePerGas = RLPEncoder.encodeUint256(_transaction.maxFeePerGas); + bytes memory encodedGasLimit = RLPEncoder.encodeUint256(_transaction.gasLimit); + // "to" field is empty if it is EVM deploy tx + bytes memory encodedTo = _transaction.reserved[1] == 1 + ? bytes(hex"80") + : RLPEncoder.encodeAddress(address(uint160(_transaction.to))); + bytes memory encodedValue = RLPEncoder.encodeUint256(_transaction.value); + // solhint-disable-next-line func-named-parameters + encodedFixedLengthParams = bytes.concat( + encodedChainId, + encodedNonce, + encodedMaxPriorityFeePerGas, + encodedMaxFeePerGas, + encodedGasLimit, + encodedTo, + encodedValue + ); + } + + // Encode only the length of the transaction data, and not the data itself, + // so as not to copy to memory a potentially huge transaction data twice. + bytes memory encodedDataLength; + { + // Safe cast, because the length of the transaction data can't be so large. + uint64 txDataLen = uint64(_transaction.data.length); + if (txDataLen != 1) { + // If the length is not equal to one, then only using the length can it be encoded definitely. + encodedDataLength = RLPEncoder.encodeNonSingleBytesLen(txDataLen); + } else if (_transaction.data[0] >= 0x80) { + // If input is a byte in [0x80, 0xff] range, RLP encoding will concatenates 0x81 with the byte. + encodedDataLength = hex"81"; + } + // Otherwise the length is not encoded at all. + } + + // On ZKsync, access lists are always zero length (at least for now). + bytes memory encodedAccessListLength = RLPEncoder.encodeListLen(0); + + // Authorization list is provided ABI-encoded in `reservedDynamic` field. + // We need to re-pack it into RLP representation. + AuthorizationListItem[] memory authList = abi.decode(_transaction.reservedDynamic, (AuthorizationListItem[])); + bytes memory encodedAuthList = new bytes(0); + unchecked { + for (uint i = 0; i < authList.length; i++) { + bytes memory encodedChainId = RLPEncoder.encodeUint256(authList[i].chainId); + bytes memory encodedNonce = RLPEncoder.encodeUint256(authList[i].nonce); + bytes memory encodedAddress = RLPEncoder.encodeAddress(authList[i].addr); + bytes memory encodedYParity = RLPEncoder.encodeUint256(authList[i].yParity); + bytes memory encodedR = RLPEncoder.encodeUint256(authList[i].r); + bytes memory encodedS = RLPEncoder.encodeUint256(authList[i].s); + uint256 itemLength = encodedChainId.length + + encodedNonce.length + + encodedAddress.length + + encodedYParity.length + + encodedR.length + + encodedS.length; + bytes memory encodedItemLength = RLPEncoder.encodeListLen(uint64(itemLength)); + // solhint-disable-next-line func-named-parameters + encodedAuthList = bytes.concat( + encodedAuthList, + encodedItemLength, + encodedChainId, + encodedAddress, + encodedNonce, + encodedYParity, + encodedR, + encodedS + ); + } + } + bytes memory encodedAuthListLength = RLPEncoder.encodeListLen(uint64(encodedAuthList.length)); + + bytes memory rEncoded; + { + uint256 rInt = uint256(bytes32(_transaction.signature[0:32])); + rEncoded = RLPEncoder.encodeUint256(rInt); + } + bytes memory sEncoded; + { + uint256 sInt = uint256(bytes32(_transaction.signature[32:64])); + sEncoded = RLPEncoder.encodeUint256(sInt); + } + bytes memory vEncoded; + { + uint256 vInt = uint256(uint8(_transaction.signature[64])); + if (vInt != 27 && vInt != 28) { + revert InvalidSig(SigField.V, vInt); + } + + vEncoded = RLPEncoder.encodeUint256(vInt - 27); + } + + bytes memory encodedListLength; + unchecked { + uint256 listLength = encodedFixedLengthParams.length + + encodedDataLength.length + + _transaction.data.length + + encodedAccessListLength.length + + encodedAuthListLength.length + + encodedAuthList.length + + rEncoded.length + + sEncoded.length + + vEncoded.length; + + // Safe cast, because the length of the list can't be so large. + encodedListLength = RLPEncoder.encodeListLen(uint64(listLength)); + } + + return + keccak256( + // solhint-disable-next-line func-named-parameters + bytes.concat( + "\x04", + encodedListLength, + encodedFixedLengthParams, + encodedDataLength, + _transaction.data, + encodedAccessListLength, + encodedAuthListLength, + encodedAuthList, + vEncoded, + rEncoded, + sEncoded + ) + ); + } } diff --git a/system-contracts/contracts/libraries/TransactionHelper.sol b/system-contracts/contracts/libraries/TransactionHelper.sol index 9eccfd93e4..7dc430ed5c 100644 --- a/system-contracts/contracts/libraries/TransactionHelper.sol +++ b/system-contracts/contracts/libraries/TransactionHelper.sol @@ -20,6 +20,8 @@ uint8 constant LEGACY_TX_TYPE = 0x0; uint8 constant EIP_2930_TX_TYPE = 0x01; /// @dev The type id of EIP1559 transactions. uint8 constant EIP_1559_TX_TYPE = 0x02; +/// @dev The type id of EIP7702 transactions. +uint8 constant EIP_7702_TX_TYPE = 0x04; /// @dev The type id of L1 to L2 transactions. uint8 constant L1_TO_L2_TX_TYPE = 0xFF; @@ -72,6 +74,18 @@ struct Transaction { bytes reservedDynamic; } +/// @notice EIP-7702 authorization list item +/// @dev Authorization list items are passed from the transaction +/// through the bootloader. +struct AuthorizationListItem { + uint256 chainId; + address addr; + uint256 nonce; + uint256 yParity; + uint256 r; + uint256 s; +} + /** * @author Matter Labs * @custom:security-contact security@matterlabs.dev @@ -109,6 +123,8 @@ library TransactionHelper { resultHash = _encodeHashEIP1559Transaction(_transaction); } else if (_transaction.txType == EIP_2930_TX_TYPE) { resultHash = _encodeHashEIP2930Transaction(_transaction); + } else if (_transaction.txType == EIP_7702_TX_TYPE) { + resultHash = _encodeHashEIP7702Transaction(_transaction); } else { // Currently no other transaction types are supported. // Any new transaction types will be processed in a similar manner. @@ -374,6 +390,122 @@ library TransactionHelper { ); } + /// @notice Encode hash of the EIP7702 transaction type. + /// @return keccak256 of the serialized RLP encoded representation of transaction + function _encodeHashEIP7702Transaction(Transaction calldata _transaction) private view returns (bytes32) { + // Signing hash of EIP1559 transactions is encoded the following way: + // H(0x04 || RLP(chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, destination, value, data, access_list, authorization_list)) + // + // Note, that on ZKsync access lists are not supported and should always be empty. + // However, the authorization list is supported and taken into account. + + // Encode all fixed-length params to avoid "stack too deep error" + bytes memory encodedFixedLengthParams; + { + bytes memory encodedChainId = RLPEncoder.encodeUint256(block.chainid); + bytes memory encodedNonce = RLPEncoder.encodeUint256(_transaction.nonce); + bytes memory encodedMaxPriorityFeePerGas = RLPEncoder.encodeUint256(_transaction.maxPriorityFeePerGas); + bytes memory encodedMaxFeePerGas = RLPEncoder.encodeUint256(_transaction.maxFeePerGas); + bytes memory encodedGasLimit = RLPEncoder.encodeUint256(_transaction.gasLimit); + // "to" field is empty if it is EVM deploy tx + bytes memory encodedTo = _transaction.reserved[1] == 1 + ? bytes(hex"80") + : RLPEncoder.encodeAddress(address(uint160(_transaction.to))); + bytes memory encodedValue = RLPEncoder.encodeUint256(_transaction.value); + // solhint-disable-next-line func-named-parameters + encodedFixedLengthParams = bytes.concat( + encodedChainId, + encodedNonce, + encodedMaxPriorityFeePerGas, + encodedMaxFeePerGas, + encodedGasLimit, + encodedTo, + encodedValue + ); + } + + // Encode only the length of the transaction data, and not the data itself, + // so as not to copy to memory a potentially huge transaction data twice. + bytes memory encodedDataLength; + { + // Safe cast, because the length of the transaction data can't be so large. + uint64 txDataLen = uint64(_transaction.data.length); + if (txDataLen != 1) { + // If the length is not equal to one, then only using the length can it be encoded definitely. + encodedDataLength = RLPEncoder.encodeNonSingleBytesLen(txDataLen); + } else if (_transaction.data[0] >= 0x80) { + // If input is a byte in [0x80, 0xff] range, RLP encoding will concatenates 0x81 with the byte. + encodedDataLength = hex"81"; + } + // Otherwise the length is not encoded at all. + } + + // On ZKsync, access lists are always zero length (at least for now). + bytes memory encodedAccessListLength = RLPEncoder.encodeListLen(0); + + // Authorization list is provided ABI-encoded in `reservedDynamic` field. + // We need to re-pack it into RLP representation. + AuthorizationListItem[] memory authList = abi.decode(_transaction.reservedDynamic, (AuthorizationListItem[])); + bytes memory encodedAuthList = new bytes(0); + unchecked { + for (uint i = 0; i < authList.length; i++) { + bytes memory encodedChainId = RLPEncoder.encodeUint256(authList[i].chainId); + bytes memory encodedAddress = RLPEncoder.encodeAddress(authList[i].addr); + bytes memory encodedNonce = RLPEncoder.encodeUint256(authList[i].nonce); + bytes memory encodedYParity = RLPEncoder.encodeUint256(authList[i].yParity); + bytes memory encodedR = RLPEncoder.encodeUint256(authList[i].r); + bytes memory encodedS = RLPEncoder.encodeUint256(authList[i].s); + uint256 itemLength = encodedChainId.length + + encodedNonce.length + + encodedAddress.length + + encodedYParity.length + + encodedR.length + + encodedS.length; + bytes memory encodedItemLength = RLPEncoder.encodeListLen(uint64(itemLength)); + // solhint-disable-next-line func-named-parameters + encodedAuthList = bytes.concat( + encodedAuthList, + encodedItemLength, + encodedChainId, + encodedAddress, + encodedNonce, + encodedYParity, + encodedR, + encodedS + ); + } + } + bytes memory encodedAuthListLength = RLPEncoder.encodeListLen(uint64(encodedAuthList.length)); + + bytes memory encodedListLength; + unchecked { + uint256 listLength = encodedFixedLengthParams.length + + encodedDataLength.length + + _transaction.data.length + + encodedAccessListLength.length + + encodedAuthListLength.length + + encodedAuthList.length; + + // Safe cast, because the length of the list can't be so large. + encodedListLength = RLPEncoder.encodeListLen(uint64(listLength)); + } + + return + keccak256( + // solhint-disable-next-line func-named-parameters + bytes.concat( + "\x04", + encodedListLength, + encodedFixedLengthParams, + encodedDataLength, + _transaction.data, + encodedAccessListLength, + encodedAuthListLength, + encodedAuthList + ) + ); + } + /// @notice Processes the common paymaster flows, e.g. setting proper allowance /// for tokens, etc. For more information on the expected behavior, check out /// the "Paymaster flows" section in the documentation. diff --git a/system-contracts/scripts/preprocess-bootloader.ts b/system-contracts/scripts/preprocess-bootloader.ts index 52b36aa4bd..0e1dc727c2 100644 --- a/system-contracts/scripts/preprocess-bootloader.ts +++ b/system-contracts/scripts/preprocess-bootloader.ts @@ -67,8 +67,9 @@ const params = { VALIDATE_TX_SELECTOR: getSelector("IAccount", "validateTransaction"), EXECUTE_TX_SELECTOR: getSelector("DefaultAccount", "executeTransaction"), RIGHT_PADDED_GET_ACCOUNT_VERSION_SELECTOR: getPaddedSelector("ContractDeployer", "extendedAccountVersion"), + RIGHT_PADDED_GET_RAW_CODE_HASH_SELECTOR: getPaddedSelector("AccountCodeStorage", "getRawCodeHash"), RIGHT_PADDED_IS_ACCOUNT_EOA_SELECTOR: getPaddedSelector("AccountCodeStorage", "isAccountEOA"), - RIGHT_PADDED_PROCESS_DELEGATIONS_SELECTOR: getPaddedSelector("AccountCodeStorage", "processDelegations"), + PROCESS_DELEGATIONS_SELECTOR: getSelector("AccountCodeStorage", "processDelegations"), PAY_FOR_TX_SELECTOR: getSelector("DefaultAccount", "payForTransaction"), PRE_PAYMASTER_SELECTOR: getSelector("DefaultAccount", "prepareForPaymaster"), VALIDATE_AND_PAY_PAYMASTER: getSelector("IPaymaster", "validateAndPayForPaymasterTransaction"), From cb98f1b2a98e52b57e51436ce8e61450236dd341 Mon Sep 17 00:00:00 2001 From: Igor Aleksanov Date: Wed, 30 Apr 2025 16:37:16 +0400 Subject: [PATCH 06/11] Delegation works (hopefully) --- .../contracts/AccountCodeStorage.sol | 162 +++++++++++------- 1 file changed, 97 insertions(+), 65 deletions(-) diff --git a/system-contracts/contracts/AccountCodeStorage.sol b/system-contracts/contracts/AccountCodeStorage.sol index fa10f29605..ae14f5b314 100644 --- a/system-contracts/contracts/AccountCodeStorage.sol +++ b/system-contracts/contracts/AccountCodeStorage.sol @@ -4,12 +4,17 @@ pragma solidity 0.8.24; import {IAccountCodeStorage} from "./interfaces/IAccountCodeStorage.sol"; import {SystemContractBase} from "./abstract/SystemContractBase.sol"; +import {SystemContractsCaller} from "./libraries/SystemContractsCaller.sol"; +import {EfficientCall} from "./libraries/EfficientCall.sol"; import {Transaction, AuthorizationListItem} from "./libraries/TransactionHelper.sol"; import {RLPEncoder} from "./libraries/RLPEncoder.sol"; import {Utils} from "./libraries/Utils.sol"; -import {DEPLOYER_SYSTEM_CONTRACT, NONCE_HOLDER_SYSTEM_CONTRACT, CURRENT_MAX_PRECOMPILE_ADDRESS, EVM_HASHES_STORAGE} from "./Constants.sol"; +import {DEPLOYER_SYSTEM_CONTRACT, NONCE_HOLDER_SYSTEM_CONTRACT, CURRENT_MAX_PRECOMPILE_ADDRESS, EVM_HASHES_STORAGE, INonceHolder} from "./Constants.sol"; import {Unauthorized, InvalidCodeHash, CodeHashReason} from "./SystemContractErrors.sol"; +event AccountDelegated(address indexed authority, address indexed delegationAddress); +event AccountDelegationRemoved(address indexed authority); + /** * @author Matter Labs * @custom:security-contact security@matterlabs.dev @@ -170,70 +175,97 @@ contract AccountCodeStorage is IAccountCodeStorage, SystemContractBase { } function processDelegations(AuthorizationListItem[] calldata authorizationList) external onlyCallFromBootloader { - emit Log("processDelegations"); - emit Log("Number of delegations"); - emit LogNumber(authorizationList.length); - - // for (uint256 i = 0; i < authorizationList.length; i++) { - // // Per EIP7702 rules, if any check for the tuple item fails, - // // we must move on to the next item in the list. - // AuthorizationListItem calldata item = authorizationList[i]; - - // // Verify the chain ID is 0 or the ID of the current chain. - // if (item.chainId != 0 || item.chainId != block.chainid) { - // continue; - // } - - // // Verify the nonce is less than 2**64 - 1. - // if (item.nonce >= 0xFFFFFFFFFFFFFFFF) { - // continue; - // } - - // // Calculate EIP7702 magic: - // // msg = keccak(MAGIC || rlp([chain_id, address, nonce])) - // bytes memory chainIdEncoded = RLPEncoder.encodeUint256(item.chainId); - // bytes memory addressEncoded = RLPEncoder.encodeAddress(item.addr); - // bytes memory nonceEncoded = RLPEncoder.encodeUint256(item.nonce); - // bytes memory listLenEncoded = RLPEncoder.encodeListLen( - // uint64(chainIdEncoded.length + addressEncoded.length + nonceEncoded.length) - // ); - // bytes32 message = keccak256( - // bytes.concat(bytes1(0x05), listLenEncoded, chainIdEncoded, addressEncoded, nonceEncoded) - // ); - - // // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature - // // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines - // // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most - // // signatures from current libraries generate a unique signature with an s-value in the lower half order. - // // - // // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value - // // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or - // // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept - // // these malleable signatures as well. - // if (uint256(item.s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) { - // continue; - // } - - // address authority = ecrecover(message, uint8(item.yParity + 27), bytes32(item.r), bytes32(item.s)); - // emit Log("Authority"); - // emit LogAddr(authority); - - // // ZKsync has native account abstraction, so we only allow delegation for EOAs. - // if (!this.isAccountEOA(authority)) { - // continue; - // } - - // if (item.nonce != NONCE_HOLDER_SYSTEM_CONTRACT.getRawNonce(item.addr)) { - // emit Log("Nonce mismatch"); - // continue; - // } - // delegatedEOAs[item.addr] = true; - // // TODO: set code hash - // // TODO: increment nonce - // } + for (uint256 i = 0; i < authorizationList.length; i++) { + // Per EIP7702 rules, if any check for the tuple item fails, + // we must move on to the next item in the list. + AuthorizationListItem calldata item = authorizationList[i]; + + // Verify the chain ID is 0 or the ID of the current chain. + if (item.chainId != 0 && item.chainId != block.chainid) { + continue; + } + + // Verify the nonce is less than 2**64 - 1. + if (item.nonce >= 0xFFFFFFFFFFFFFFFF) { + continue; + } + + // Calculate EIP7702 magic: + // msg = keccak(MAGIC || rlp([chain_id, address, nonce])) + bytes memory chainIdEncoded = RLPEncoder.encodeUint256(item.chainId); + bytes memory addressEncoded = RLPEncoder.encodeAddress(item.addr); + bytes memory nonceEncoded = RLPEncoder.encodeUint256(item.nonce); + bytes memory listLenEncoded = RLPEncoder.encodeListLen( + uint64(chainIdEncoded.length + addressEncoded.length + nonceEncoded.length) + ); + bytes32 message = keccak256( + bytes.concat(bytes1(0x05), listLenEncoded, chainIdEncoded, addressEncoded, nonceEncoded) + ); + + // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature + // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines + // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most + // signatures from current libraries generate a unique signature with an s-value in the lower half order. + // + // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value + // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or + // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept + // these malleable signatures as well. + if (uint256(item.s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) { + continue; + } + + address authority = ecrecover(message, uint8(item.yParity + 27), bytes32(item.r), bytes32(item.s)); + + // ZKsync has native account abstraction, so we only allow delegation for EOAs. + if (!this.isAccountEOA(authority)) { + continue; + } + + bool nonceIncremented = this._performRawMimicCall( + uint32(gasleft()), + authority, + address(NONCE_HOLDER_SYSTEM_CONTRACT), + abi.encodeCall(INonceHolder.incrementMinNonceIfEquals, (item.nonce)), + true + ); + if (!nonceIncremented) { + continue; + } + if (item.addr == address(0)) { + // If the delegation address is 0, we need to remove the delegation. + delete delegatedEOAs[authority]; + emit AccountDelegationRemoved(authority); + } else { + // Otherwise, load code hash of the delegation address and store it in the account. + bytes32 codeHash = getRawCodeHash(item.addr); + // TODO: Do we need any security checks here, e.g. non-default code hash or non-system contract? + _storeCodeHash(authority, codeHash); + delegatedEOAs[item.addr] = true; + emit AccountDelegated(authority, item.addr); + } + } + } + + + // Needed to convert `memory` to `calldata` + // TODO: (partial) duplication with EntryPointV01; probably need to be moved somewhere. + function _performRawMimicCall( + uint32 _gas, + address _whoToMimic, + address _to, + bytes calldata _data, + bool isSystem + ) external onlyCallFrom(address(this)) returns (bool success) { + return + EfficientCall.rawMimicCall( + _gas, + _to, + _data, + _whoToMimic, + false, + isSystem + ); } } -event Log(string); -event LogAddr(address); -event LogNumber(uint256); From a7890d86a8312524067598740eee293b91cd807b Mon Sep 17 00:00:00 2001 From: Igor Aleksanov Date: Wed, 30 Apr 2025 17:45:42 +0400 Subject: [PATCH 07/11] Delegation works --- system-contracts/bootloader/bootloader.yul | 28 +++++++++-- .../contracts/AccountCodeStorage.sol | 47 +++++++++---------- .../interfaces/IAccountCodeStorage.sol | 2 +- .../scripts/preprocess-bootloader.ts | 3 +- 4 files changed, 50 insertions(+), 30 deletions(-) diff --git a/system-contracts/bootloader/bootloader.yul b/system-contracts/bootloader/bootloader.yul index bce188d3dc..9d1049eb48 100644 --- a/system-contracts/bootloader/bootloader.yul +++ b/system-contracts/bootloader/bootloader.yul @@ -694,10 +694,10 @@ object "Bootloader" { ret := mload(0) } - /// @notice Returns whether the account is EOA (even if it's EIP-7702 delegated). + /// @notice Returns whether the account is a EIP-7702 delegated EOA . /// @param addr The address of the account to check. - function isEOA(addr) -> ret { - mstore(0, {{RIGHT_PADDED_IS_ACCOUNT_EOA_SELECTOR}}) + function isAccountDelegated(addr) -> ret { + mstore(0, {{RIGHT_PADDED_IS_ACCOUNT_DELEGATED_SELECTOR}}) mstore(4, addr) let success := staticcall( gas(), @@ -2229,6 +2229,16 @@ object "Bootloader" { } } + /// @dev Checks whether an address is an EOA (i.e. has not code deployed on it) + /// @param addr The address to check + function isEOA(addr) -> ret { + ret := 0 + + if gt(addr, MAX_SYSTEM_CONTRACT_ADDR()) { + ret := iszero(getRawCodeHash(addr, false)) + } + } + /// @dev Calls the `payForTransaction` method of an account function accountPayForTx(account, txDataOffset) -> success { success := callAccountMethod({{PAY_FOR_TX_SELECTOR}}, account, txDataOffset) @@ -2613,7 +2623,17 @@ object "Bootloader" { /// @dev Function responsible for the execution of the L2 transaction /// @dev Returns `true` or `false` depending on whether or not the tx has reverted. function executeL2Tx(txDataOffset, from) -> ret { - ret := callAccountMethod({{EXECUTE_TX_SELECTOR}}, from, txDataOffset) + let isDelegated := isAccountDelegated(from) + + switch isDelegated + case 0 { + // Account not delegated: invoke the `execute` method + ret := callAccountMethod({{EXECUTE_TX_SELECTOR}}, from, txDataOffset) + } + default { + // Account is delegated: invoke through AccountCodeStorage + ret := callAccountMethod({{DELEGATE_TX_SELECTOR}}, ACCOUNT_CODE_STORAGE_ADDR(), txDataOffset) + } if iszero(ret) { debugReturndata() diff --git a/system-contracts/contracts/AccountCodeStorage.sol b/system-contracts/contracts/AccountCodeStorage.sol index ae14f5b314..09840ff054 100644 --- a/system-contracts/contracts/AccountCodeStorage.sol +++ b/system-contracts/contracts/AccountCodeStorage.sol @@ -32,10 +32,8 @@ contract AccountCodeStorage is IAccountCodeStorage, SystemContractBase { bytes32 private constant EMPTY_STRING_KECCAK = 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470; /// @notice Information about EIP-7702 delegated EOAs. - /// @dev Delegated EOAs have code hash set to the code hash of the delegation address contract. - /// So, in order to check whether an account is an EOA, checking for the code hash is not enough. - /// We need to also check whether the account is a delegated EOA. - mapping(address => bool) private delegatedEOAs; + /// @dev Delegated EOAs. + mapping(address => address) private delegatedEOAs; modifier onlyDeployer() { if (msg.sender != address(DEPLOYER_SYSTEM_CONTRACT)) { @@ -168,10 +166,8 @@ contract AccountCodeStorage is IAccountCodeStorage, SystemContractBase { return Utils.isCodeHashEVM(bytecodeHash); } - /// @notice Method for detecting whether an address is an EOA - /// @dev Checks whether the account either has no code hash set or is a delegated EOA. - function isAccountEOA(address _addr) external view override returns (bool) { - return delegatedEOAs[_addr] || getRawCodeHash(_addr) == 0x00; + function isAccountDelegated(address _addr) external view override returns (bool) { + return delegatedEOAs[_addr] != address(0); } function processDelegations(AuthorizationListItem[] calldata authorizationList) external onlyCallFromBootloader { @@ -218,7 +214,7 @@ contract AccountCodeStorage is IAccountCodeStorage, SystemContractBase { address authority = ecrecover(message, uint8(item.yParity + 27), bytes32(item.r), bytes32(item.s)); // ZKsync has native account abstraction, so we only allow delegation for EOAs. - if (!this.isAccountEOA(authority)) { + if (this.getRawCodeHash(authority) != 0x00) { continue; } @@ -237,17 +233,29 @@ contract AccountCodeStorage is IAccountCodeStorage, SystemContractBase { delete delegatedEOAs[authority]; emit AccountDelegationRemoved(authority); } else { - // Otherwise, load code hash of the delegation address and store it in the account. - bytes32 codeHash = getRawCodeHash(item.addr); + // Otherwise, store the delegation. // TODO: Do we need any security checks here, e.g. non-default code hash or non-system contract? - _storeCodeHash(authority, codeHash); - delegatedEOAs[item.addr] = true; + delegatedEOAs[authority] = item.addr; emit AccountDelegated(authority, item.addr); } } } - + // TODO: Function made to match signature of account methods for simplicity. + function delegateTx( + bytes32, + bytes32, + Transaction calldata _tx + ) external onlyCallFromBootloader returns (bool success) { + // Check if the transaction is delegated + address from = address(uint160(_tx.from)); + address delegationAddress = delegatedEOAs[from]; + if (delegationAddress == address(0)) { + return false; + } + return this._performRawMimicCall(uint32(gasleft()), from, delegationAddress, _tx.data, false); + } + // Needed to convert `memory` to `calldata` // TODO: (partial) duplication with EntryPointV01; probably need to be moved somewhere. function _performRawMimicCall( @@ -257,15 +265,6 @@ contract AccountCodeStorage is IAccountCodeStorage, SystemContractBase { bytes calldata _data, bool isSystem ) external onlyCallFrom(address(this)) returns (bool success) { - return - EfficientCall.rawMimicCall( - _gas, - _to, - _data, - _whoToMimic, - false, - isSystem - ); + return EfficientCall.rawMimicCall(_gas, _to, _data, _whoToMimic, false, isSystem); } } - diff --git a/system-contracts/contracts/interfaces/IAccountCodeStorage.sol b/system-contracts/contracts/interfaces/IAccountCodeStorage.sol index 19b032a9e0..5cc5731235 100644 --- a/system-contracts/contracts/interfaces/IAccountCodeStorage.sol +++ b/system-contracts/contracts/interfaces/IAccountCodeStorage.sol @@ -17,5 +17,5 @@ interface IAccountCodeStorage { function isAccountEVM(address _addr) external view returns (bool); - function isAccountEOA(address _addr) external view returns (bool); + function isAccountDelegated(address _addr) external view returns (bool); } diff --git a/system-contracts/scripts/preprocess-bootloader.ts b/system-contracts/scripts/preprocess-bootloader.ts index 0e1dc727c2..9497bfadf3 100644 --- a/system-contracts/scripts/preprocess-bootloader.ts +++ b/system-contracts/scripts/preprocess-bootloader.ts @@ -68,7 +68,8 @@ const params = { EXECUTE_TX_SELECTOR: getSelector("DefaultAccount", "executeTransaction"), RIGHT_PADDED_GET_ACCOUNT_VERSION_SELECTOR: getPaddedSelector("ContractDeployer", "extendedAccountVersion"), RIGHT_PADDED_GET_RAW_CODE_HASH_SELECTOR: getPaddedSelector("AccountCodeStorage", "getRawCodeHash"), - RIGHT_PADDED_IS_ACCOUNT_EOA_SELECTOR: getPaddedSelector("AccountCodeStorage", "isAccountEOA"), + RIGHT_PADDED_IS_ACCOUNT_DELEGATED_SELECTOR: getPaddedSelector("AccountCodeStorage", "isAccountDelegated"), + DELEGATE_TX_SELECTOR: getSelector("AccountCodeStorage", "delegateTx"), PROCESS_DELEGATIONS_SELECTOR: getSelector("AccountCodeStorage", "processDelegations"), PAY_FOR_TX_SELECTOR: getSelector("DefaultAccount", "payForTransaction"), PRE_PAYMASTER_SELECTOR: getSelector("DefaultAccount", "prepareForPaymaster"), From 1eee5923e1706d30348cf09fb5f7a06d4ec18cb4 Mon Sep 17 00:00:00 2001 From: Igor Aleksanov Date: Tue, 6 May 2025 11:11:35 +0400 Subject: [PATCH 08/11] Delegate 7702 through mimiccall --- system-contracts/bootloader/bootloader.yul | 27 ++++++++++++++----- .../contracts/AccountCodeStorage.sol | 22 ++++----------- .../interfaces/IAccountCodeStorage.sol | 2 +- .../scripts/preprocess-bootloader.ts | 3 +-- 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/system-contracts/bootloader/bootloader.yul b/system-contracts/bootloader/bootloader.yul index 9d1049eb48..9d1e8d8309 100644 --- a/system-contracts/bootloader/bootloader.yul +++ b/system-contracts/bootloader/bootloader.yul @@ -694,10 +694,11 @@ object "Bootloader" { ret := mload(0) } - /// @notice Returns whether the account is a EIP-7702 delegated EOA . + /// @notice Returns the address of EIP-7702 delegation for the account (or zero, if account + /// is not delegated). /// @param addr The address of the account to check. - function isAccountDelegated(addr) -> ret { - mstore(0, {{RIGHT_PADDED_IS_ACCOUNT_DELEGATED_SELECTOR}}) + function getDelegationAddress(addr) -> ret { + mstore(0, {{RIGHT_PADDED_GET_ACCOUNT_DELEGATION_SELECTOR}}) mstore(4, addr) let success := staticcall( gas(), @@ -2005,6 +2006,15 @@ object "Bootloader" { debugLog("from: ", from) debugLog("to: ", to) + let delegation := getDelegationAddress(from) + debugLog("delegation: ", delegation) + + if gt(delegation, 0) { + // If the delegation is not zero, we need to invoke the delegation + // target instead of the original `to` field. + to := delegation + } + switch isEOA(from) case true { setTxOrigin(from) @@ -2623,16 +2633,19 @@ object "Bootloader" { /// @dev Function responsible for the execution of the L2 transaction /// @dev Returns `true` or `false` depending on whether or not the tx has reverted. function executeL2Tx(txDataOffset, from) -> ret { - let isDelegated := isAccountDelegated(from) + let delegation := getDelegationAddress(from) - switch isDelegated + switch delegation case 0 { // Account not delegated: invoke the `execute` method ret := callAccountMethod({{EXECUTE_TX_SELECTOR}}, from, txDataOffset) } default { - // Account is delegated: invoke through AccountCodeStorage - ret := callAccountMethod({{DELEGATE_TX_SELECTOR}}, ACCOUNT_CODE_STORAGE_ADDR(), txDataOffset) + // Account is delegated: invoke through mimicall using calldata provided + let innerTxDataOffset := add(txDataOffset, 32) + let calldataPtr := getDataPtr(innerTxDataOffset) + let value := getValue(innerTxDataOffset) + ret := msgValueSimulatorMimicCall(delegation, from, value, calldataPtr) } if iszero(ret) { diff --git a/system-contracts/contracts/AccountCodeStorage.sol b/system-contracts/contracts/AccountCodeStorage.sol index 09840ff054..f88890805b 100644 --- a/system-contracts/contracts/AccountCodeStorage.sol +++ b/system-contracts/contracts/AccountCodeStorage.sol @@ -166,8 +166,11 @@ contract AccountCodeStorage is IAccountCodeStorage, SystemContractBase { return Utils.isCodeHashEVM(bytecodeHash); } - function isAccountDelegated(address _addr) external view override returns (bool) { - return delegatedEOAs[_addr] != address(0); + /// @notice Returns the address of the account that is delegated to execute transactions on behalf of the given + /// address. + /// @notice Returns the zero address if no delegation is set. + function getAccountDelegation(address _addr) external view override returns (address) { + return delegatedEOAs[_addr]; } function processDelegations(AuthorizationListItem[] calldata authorizationList) external onlyCallFromBootloader { @@ -241,21 +244,6 @@ contract AccountCodeStorage is IAccountCodeStorage, SystemContractBase { } } - // TODO: Function made to match signature of account methods for simplicity. - function delegateTx( - bytes32, - bytes32, - Transaction calldata _tx - ) external onlyCallFromBootloader returns (bool success) { - // Check if the transaction is delegated - address from = address(uint160(_tx.from)); - address delegationAddress = delegatedEOAs[from]; - if (delegationAddress == address(0)) { - return false; - } - return this._performRawMimicCall(uint32(gasleft()), from, delegationAddress, _tx.data, false); - } - // Needed to convert `memory` to `calldata` // TODO: (partial) duplication with EntryPointV01; probably need to be moved somewhere. function _performRawMimicCall( diff --git a/system-contracts/contracts/interfaces/IAccountCodeStorage.sol b/system-contracts/contracts/interfaces/IAccountCodeStorage.sol index 5cc5731235..adfb1c36a4 100644 --- a/system-contracts/contracts/interfaces/IAccountCodeStorage.sol +++ b/system-contracts/contracts/interfaces/IAccountCodeStorage.sol @@ -17,5 +17,5 @@ interface IAccountCodeStorage { function isAccountEVM(address _addr) external view returns (bool); - function isAccountDelegated(address _addr) external view returns (bool); + function getAccountDelegation(address _addr) external view returns (address); } diff --git a/system-contracts/scripts/preprocess-bootloader.ts b/system-contracts/scripts/preprocess-bootloader.ts index 9497bfadf3..0def983133 100644 --- a/system-contracts/scripts/preprocess-bootloader.ts +++ b/system-contracts/scripts/preprocess-bootloader.ts @@ -68,8 +68,7 @@ const params = { EXECUTE_TX_SELECTOR: getSelector("DefaultAccount", "executeTransaction"), RIGHT_PADDED_GET_ACCOUNT_VERSION_SELECTOR: getPaddedSelector("ContractDeployer", "extendedAccountVersion"), RIGHT_PADDED_GET_RAW_CODE_HASH_SELECTOR: getPaddedSelector("AccountCodeStorage", "getRawCodeHash"), - RIGHT_PADDED_IS_ACCOUNT_DELEGATED_SELECTOR: getPaddedSelector("AccountCodeStorage", "isAccountDelegated"), - DELEGATE_TX_SELECTOR: getSelector("AccountCodeStorage", "delegateTx"), + RIGHT_PADDED_GET_ACCOUNT_DELEGATION_SELECTOR: getPaddedSelector("AccountCodeStorage", "getAccountDelegation"), PROCESS_DELEGATIONS_SELECTOR: getSelector("AccountCodeStorage", "processDelegations"), PAY_FOR_TX_SELECTOR: getSelector("DefaultAccount", "payForTransaction"), PRE_PAYMASTER_SELECTOR: getSelector("DefaultAccount", "prepareForPaymaster"), From 55b189150f8d514b61a91a78c145861558a75eec Mon Sep 17 00:00:00 2001 From: Igor Aleksanov Date: Tue, 6 May 2025 11:44:49 +0400 Subject: [PATCH 09/11] Fixes after merge --- system-contracts/contracts/erc4337/EntryPointV01.sol | 2 +- system-contracts/contracts/interfaces/IEntryPoint.sol | 2 +- system-contracts/scripts/constants.ts | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/system-contracts/contracts/erc4337/EntryPointV01.sol b/system-contracts/contracts/erc4337/EntryPointV01.sol index 204a1fbd24..ac879bdf5d 100644 --- a/system-contracts/contracts/erc4337/EntryPointV01.sol +++ b/system-contracts/contracts/erc4337/EntryPointV01.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.24; +pragma solidity ^0.8.20; import {IEntryPoint, PackedUserOperation} from "../interfaces/IEntryPoint.sol"; import {IBootloaderUtilities} from "../interfaces/IBootloaderUtilities.sol"; diff --git a/system-contracts/contracts/interfaces/IEntryPoint.sol b/system-contracts/contracts/interfaces/IEntryPoint.sol index 01c1e9aa02..e5578713f6 100644 --- a/system-contracts/contracts/interfaces/IEntryPoint.sol +++ b/system-contracts/contracts/interfaces/IEntryPoint.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma solidity 0.8.24; +pragma solidity ^0.8.20; import {Transaction} from "../libraries/TransactionHelper.sol"; diff --git a/system-contracts/scripts/constants.ts b/system-contracts/scripts/constants.ts index adea0693d5..34b899dbb0 100644 --- a/system-contracts/scripts/constants.ts +++ b/system-contracts/scripts/constants.ts @@ -197,6 +197,7 @@ export const SYSTEM_CONTRACTS: ISystemContracts = { address: "0x0000000000000000000000000000000000008016", codeName: "EntryPoint", lang: Language.Solidity, + location: SourceLocation.SystemContracts, }, create2Factory: { // This is explicitly a non-system-contract address. From a204c5fd6a52d1749a02bd1cec8aa1b2cfa72e63 Mon Sep 17 00:00:00 2001 From: Igor Aleksanov Date: Mon, 12 May 2025 15:37:49 +0400 Subject: [PATCH 10/11] Set code hash for delegated account --- system-contracts/bootloader/bootloader.yul | 84 ++++++++++++++++++- .../contracts/AccountCodeStorage.sol | 12 ++- .../contracts/ContractDeployer.sol | 8 +- .../scripts/preprocess-bootloader.ts | 1 + 4 files changed, 97 insertions(+), 8 deletions(-) diff --git a/system-contracts/bootloader/bootloader.yul b/system-contracts/bootloader/bootloader.yul index 9d1e8d8309..40b9a580f9 100644 --- a/system-contracts/bootloader/bootloader.yul +++ b/system-contracts/bootloader/bootloader.yul @@ -694,6 +694,50 @@ object "Bootloader" { ret := mload(0) } + + + /// @notice Overrides the "raw" code hash of the address. "Raw" means that it must use exactly the value + /// that is stored in the AccountCodeStorage system contract for that address, without applying any + /// additional transformations. + /// This method is very unsafe and it shouldn't be used to do long-term modifications. + /// Right now it's only used to override the bytecode hash of delegated accounts to perform + /// transaction validation & payment. + /// @param addr The address of the account to set the code hash of. + /// @param codeHash The code hash to be set. + /// @param assertSuccess Whether to revert the bootloader if the call to the AccountCodeStorage fails. If `false`, only + /// `nearCallPanic` will be issued in case of failure, which is helpful for cases, when the reason for failure is user providing not + /// enough gas. + function setRawCodeHash(addr, codeHash, assertSuccess) -> ret { + mstore(0, {{RIGHT_PADDED_SET_RAW_CODE_HASH_SELECTOR}}) + mstore(4, addr) + mstore(36, codeHash) + let success := staticcall( + gas(), + ACCOUNT_CODE_STORAGE_ADDR(), + 0, + 36, + 0, + 32 + ) + + // In case the call to the account code storage fails, + // it most likely means that the caller did not provide enough gas for + // the call. + // In case the caller is certain that the amount of gas provided is enough, i.e. + // (`assertSuccess` = true), then we should panic. + if iszero(success) { + if assertSuccess { + // The call must've succeeded, but it didn't. So we revert the bootloader. + assertionError("getRawCodeHash failed") + } + + // Most likely not enough gas provided, revert the current frame. + nearCallPanic() + } + + ret := mload(0) + } + /// @notice Returns the address of EIP-7702 delegation for the account (or zero, if account /// is not delegated). /// @param addr The address of the account to check. @@ -2239,28 +2283,48 @@ object "Bootloader" { } } - /// @dev Checks whether an address is an EOA (i.e. has not code deployed on it) + /// @dev Checks whether an address is an EOA (i.e. has not code deployed on it or it's a 7702-delegated account) /// @param addr The address to check function isEOA(addr) -> ret { ret := 0 + let delegation := getDelegationAddress(addr) + // TODO: This logic is duplicated in several places, we should create a dedicated method. if gt(addr, MAX_SYSTEM_CONTRACT_ADDR()) { - ret := iszero(getRawCodeHash(addr, false)) + ret := or( + iszero(getRawCodeHash(addr, false)), + gt(delegation, 0) + ) } } /// @dev Calls the `payForTransaction` method of an account function accountPayForTx(account, txDataOffset) -> success { + let delegation := getDelegationAddress(account) + let rawCodeHash := 0 + if gt(delegation, 0) { + rawCodeHash := getRawCodeHash(delegation, true) + setRawCodeHash(account, 0, true) + } success := callAccountMethod({{PAY_FOR_TX_SELECTOR}}, account, txDataOffset) + if gt(delegation, 0) { + setRawCodeHash(account, rawCodeHash, true) + } } /// @dev Calls the `prepareForPaymaster` method of an account function accountPrePaymaster(account, txDataOffset) -> success { + // TODO: should we allow delegated accounts to use native paymasters? + // TODO: Gut feeling is that the answer is "NO" as we're deprecating EIP-712 txs + // TOOD: and native accounts have their own entrypoint. success := callAccountMethod({{PRE_PAYMASTER_SELECTOR}}, account, txDataOffset) } /// @dev Calls the `validateAndPayForPaymasterTransaction` method of a paymaster function validateAndPayForPaymasterTransaction(paymaster, txDataOffset) -> success { + // TODO: should we allow delegated accounts to use native paymasters? + // TODO: Gut feeling is that the answer is "NO" as we're deprecating EIP-712 txs + // TOOD: and native accounts have their own entrypoint. success := callAccountMethod({{VALIDATE_AND_PAY_PAYMASTER}}, paymaster, txDataOffset) } @@ -2501,7 +2565,21 @@ object "Bootloader" { setHook(VM_HOOK_ACCOUNT_VALIDATION_ENTERED()) debugLog("pre-validate",0) debugLog("pre-validate",from) + + // Override bytecode hash for validation if required. + // TODO: It should be safe, since delegation is only allowed for EOAs in the first place. + let delegation := getDelegationAddress(from) + let rawCodeHash := 0 + if gt(delegation, 0) { + rawCodeHash := getRawCodeHash(delegation, true) + setRawCodeHash(from, 0, true) + } + let success := callAccountMethod({{VALIDATE_TX_SELECTOR}}, from, txDataOffset) + + if gt(delegation, 0) { + setRawCodeHash(from, rawCodeHash, true) + } setHook(VM_HOOK_NO_VALIDATION_ENTERED()) if iszero(success) { @@ -2645,7 +2723,7 @@ object "Bootloader" { let innerTxDataOffset := add(txDataOffset, 32) let calldataPtr := getDataPtr(innerTxDataOffset) let value := getValue(innerTxDataOffset) - ret := msgValueSimulatorMimicCall(delegation, from, value, calldataPtr) + ret := msgValueSimulatorMimicCall(from, from, value, calldataPtr) } if iszero(ret) { diff --git a/system-contracts/contracts/AccountCodeStorage.sol b/system-contracts/contracts/AccountCodeStorage.sol index 6c7d89eb62..b40bde2a9e 100644 --- a/system-contracts/contracts/AccountCodeStorage.sol +++ b/system-contracts/contracts/AccountCodeStorage.sol @@ -173,6 +173,12 @@ contract AccountCodeStorage is IAccountCodeStorage, SystemContractBase { return delegatedEOAs[_addr]; } + /// @notice Allows the bootloader to override bytecode hash of account. + /// TODO: can we avoid it and do it in bootloader? Having it as a public interface feels very unsafe. + function setRawCodeHash(address addr, bytes32 rawBytecodeHash) external onlyCallFromBootloader { + _storeCodeHash(addr, rawBytecodeHash); + } + function processDelegations(AuthorizationListItem[] calldata authorizationList) external onlyCallFromBootloader { for (uint256 i = 0; i < authorizationList.length; i++) { // Per EIP7702 rules, if any check for the tuple item fails, @@ -217,7 +223,7 @@ contract AccountCodeStorage is IAccountCodeStorage, SystemContractBase { address authority = ecrecover(message, uint8(item.yParity + 27), bytes32(item.r), bytes32(item.s)); // ZKsync has native account abstraction, so we only allow delegation for EOAs. - if (this.getRawCodeHash(authority) != 0x00) { + if (this.getRawCodeHash(authority) != 0x00 && this.getAccountDelegation(authority) == address(0)) { continue; } @@ -234,11 +240,15 @@ contract AccountCodeStorage is IAccountCodeStorage, SystemContractBase { if (item.addr == address(0)) { // If the delegation address is 0, we need to remove the delegation. delete delegatedEOAs[authority]; + _storeCodeHash(authority, 0x00); emit AccountDelegationRemoved(authority); } else { // Otherwise, store the delegation. // TODO: Do we need any security checks here, e.g. non-default code hash or non-system contract? delegatedEOAs[authority] = item.addr; + + bytes32 codeHash = getRawCodeHash(item.addr); + _storeCodeHash(authority, codeHash); // TODO: Do we need additional checks here? emit AccountDelegated(authority, item.addr); } } diff --git a/system-contracts/contracts/ContractDeployer.sol b/system-contracts/contracts/ContractDeployer.sol index ec909f02ee..efd098f59c 100644 --- a/system-contracts/contracts/ContractDeployer.sol +++ b/system-contracts/contracts/ContractDeployer.sol @@ -56,10 +56,10 @@ contract ContractDeployer is IContractDeployer, SystemContractBase { } // It is an EOA, it is still an account. - if ( - _address > address(MAX_SYSTEM_CONTRACT_ADDRESS) && - ACCOUNT_CODE_STORAGE_SYSTEM_CONTRACT.getRawCodeHash(_address) == 0 - ) { + bool notSystem = _address > address(MAX_SYSTEM_CONTRACT_ADDRESS); + bool noCodeHash = ACCOUNT_CODE_STORAGE_SYSTEM_CONTRACT.getRawCodeHash(_address) == 0; + bool delegated = ACCOUNT_CODE_STORAGE_SYSTEM_CONTRACT.getAccountDelegation(_address) != address(0); + if (notSystem && (noCodeHash || delegated)) { return AccountAbstractionVersion.Version1; } diff --git a/system-contracts/scripts/preprocess-bootloader.ts b/system-contracts/scripts/preprocess-bootloader.ts index 0def983133..eb48ba3a02 100644 --- a/system-contracts/scripts/preprocess-bootloader.ts +++ b/system-contracts/scripts/preprocess-bootloader.ts @@ -69,6 +69,7 @@ const params = { RIGHT_PADDED_GET_ACCOUNT_VERSION_SELECTOR: getPaddedSelector("ContractDeployer", "extendedAccountVersion"), RIGHT_PADDED_GET_RAW_CODE_HASH_SELECTOR: getPaddedSelector("AccountCodeStorage", "getRawCodeHash"), RIGHT_PADDED_GET_ACCOUNT_DELEGATION_SELECTOR: getPaddedSelector("AccountCodeStorage", "getAccountDelegation"), + RIGHT_PADDED_SET_RAW_CODE_HASH_SELECTOR: getPaddedSelector("AccountCodeStorage", "setRawCodeHash"), PROCESS_DELEGATIONS_SELECTOR: getSelector("AccountCodeStorage", "processDelegations"), PAY_FOR_TX_SELECTOR: getSelector("DefaultAccount", "payForTransaction"), PRE_PAYMASTER_SELECTOR: getSelector("DefaultAccount", "prepareForPaymaster"), From 2f6735ef6b472dca539ffc859fb41cd57e968e5b Mon Sep 17 00:00:00 2001 From: Igor Aleksanov Date: Wed, 14 May 2025 14:49:41 +0400 Subject: [PATCH 11/11] Support EVM deployments in eth_call --- system-contracts/bootloader/bootloader.yul | 63 ++++++++++++++++++---- 1 file changed, 54 insertions(+), 9 deletions(-) diff --git a/system-contracts/bootloader/bootloader.yul b/system-contracts/bootloader/bootloader.yul index 40b9a580f9..067644c313 100644 --- a/system-contracts/bootloader/bootloader.yul +++ b/system-contracts/bootloader/bootloader.yul @@ -711,11 +711,12 @@ object "Bootloader" { mstore(0, {{RIGHT_PADDED_SET_RAW_CODE_HASH_SELECTOR}}) mstore(4, addr) mstore(36, codeHash) - let success := staticcall( + let success := call( gas(), ACCOUNT_CODE_STORAGE_ADDR(), 0, - 36, + 0, + 68, 0, 32 ) @@ -728,7 +729,7 @@ object "Bootloader" { if iszero(success) { if assertSuccess { // The call must've succeeded, but it didn't. So we revert the bootloader. - assertionError("getRawCodeHash failed") + assertionError("setRawCodeHash failed") } // Most likely not enough gas provided, revert the current frame. @@ -2032,6 +2033,39 @@ object "Bootloader" { + function ethCallEvmConsturction( + from, + dataPtr + ) -> success { + // Set fake address + let to := 0xF234567890123456789012345678901234567890 + // Set raw code hash to the constructing EVM contract + // so that we can get deployment bytecode as return value. + setRawCodeHash(to, 0x0201000000000000000000000000000000000000000000000000000000000000, true) + + // TODO: transfer value + + success := mimicCallOnlyResult( + to, + from, + dataPtr, + 1, // Constructor + 0, // Not a mimic call + 0, + 0, + 0, + ) + + // Returned data bytes have structure: paddedBytecode.evmBytecodeLen.constructorReturnEvmGas + // So we need to load the 2nd from last word to get the bytecode length + if success { + let returnSize := returndatasize() + returndatacopy(0,0,returnSize) + let bytecodeSize := mload(sub(returnSize, 0x40)) + return(0,bytecodeSize) + } + } + function ZKSYNC_NEAR_CALL_ethCall( abi, txDataOffset, @@ -2046,6 +2080,7 @@ object "Bootloader" { let innerTxDataOffset := add(txDataOffset, 32) let to := getTo(innerTxDataOffset) let from := getFrom(innerTxDataOffset) + let isEvmConstruction := getReserved1(innerTxDataOffset) debugLog("from: ", from) debugLog("to: ", to) @@ -2072,12 +2107,22 @@ object "Bootloader" { let value := getValue(innerTxDataOffset) - let success := msgValueSimulatorMimicCall( - to, - from, - value, - dataPtr - ) + let success := 0 + switch isEvmConstruction + case 0 { + success := msgValueSimulatorMimicCall( + to, + from, + value, + dataPtr + ) + } + default { + success := ethCallEvmConsturction( + from, + dataPtr + ) + } if iszero(success) { // If success is 0, we need to revert