diff --git a/system-contracts/bootloader/bootloader.yul b/system-contracts/bootloader/bootloader.yul index 0b2861f8f8..cf56ac76be 100644 --- a/system-contracts/bootloader/bootloader.yul +++ b/system-contracts/bootloader/bootloader.yul @@ -694,6 +694,55 @@ object "Bootloader" { ret := mload(0) } + /// @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) { + debugLog("processDelegations", 0) + // 1. Read delegation length + let ptr := getReservedDynamicPtr(innerTxDataOffset) + let length := mload(ptr) + + // We check the validity of transaction in `validateTypedTxStructure`, but this function + // is invoked for every tx. So here we're only checking if we actually need to call + // `ContractDeployer::processDelegations`. + let isEIP7702 := eq(getTxType(innerTxDataOffset), 4) + let isDelegationProvided := gt(length, 0) + let shouldProcess := and(isEIP7702, isDelegationProvided) + debugLog("shouldProcessDelegations", shouldProcess) + + 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, {{PROCESS_DELEGATIONS_SELECTOR}}) + + // 3. Call the method + let calldataOffset := add(ptr, 28) + let calldataLength := add(length, 4) + let success := call( + gas(), + CONTRACT_DEPLOYER_ADDR(), + 0, + calldataOffset, + calldataLength, + 0, + 0 + ) + + // 4. Restore the length in memory + mstore(ptr, length) + + // 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 /// sent to L1 as a message to the L1 contract that a certain operation has been processed. function getCanonicalL1TxHash(txDataOffset) -> ret { @@ -1332,6 +1381,26 @@ object "Bootloader" { revertWithReason(TX_VALIDATION_OUT_OF_GAS(), 0) } + // Processing of EIP-7702 delegations is a part of transaction validation, + // since it can update transaction nonces and should not be rolled back + // even if transaction execution fails for any reason. + let gasBeforeDelegations := gas() + let processDelegationsABI := getNearCallABI(gasLeft) + debugLog("processDelegationsABI", processDelegationsABI) + let delegationsProcessed := ZKSYNC_NEAR_CALL_processDelegations( + processDelegationsABI, + txDataOffset, + gasPrice + ) + debugLog("delegationsProcessed", delegationsProcessed) + let gasUsedForDelegations := sub(gasBeforeDelegations, gas()) + gasLeft := saturatingSub(gasLeft, gasUsedForDelegations) + debugLog("gasLeft after delegations", gasLeft) + + if iszero(delegationsProcessed) { + revertWithReason(FAILED_TO_PROCESS_EIP7702_DELEGATIONS_ERR_CODE(), 0) + } + if isNotEnoughGasForPubdata( basePubdataSpent, gasLeft, reservedGas, gasPerPubdata ) { @@ -1436,6 +1505,28 @@ object "Bootloader" { ret := 1 } + /// @dev Function responsible for the validation & fee payment step of the transaction. + /// @param abi The nearCall ABI. It is implicitly used as gasLimit for the call of this function. + /// @param txDataOffset The offset to the ABI-encoded Transaction struct. + /// @param gasPrice The gasPrice to be used in this transaction. + function ZKSYNC_NEAR_CALL_processDelegations( + abi, + txDataOffset, + gasPrice + ) -> ret { + let innerTxDataOffset := add(txDataOffset, 32) + + // For the validation step we always use the bootloader as the tx.origin of the transaction + setTxOrigin(BOOTLOADER_FORMAL_ADDR()) + setGasPrice(gasPrice) + + debugLog("Starting processing delegations", 0) + processDelegations(innerTxDataOffset) + debugLog("Processing delegations complete", 1) + + ret := 1 + } + /// @dev Function responsible for the execution of the L2 transaction. /// It includes both the call to the `executeTransaction` method of the account /// and the call to postOp of the account. @@ -2152,13 +2243,34 @@ 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 if gt(addr, MAX_SYSTEM_CONTRACT_ADDR()) { - ret := iszero(getRawCodeHash(addr, false)) + mstore(0, {{RIGHT_PADDED_IS_ACCOUNT_EOA_SELECTOR}}) + mstore(4, addr) + let success := staticcall( + gas(), + CONTRACT_DEPLOYER_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) } } @@ -2169,11 +2281,17 @@ object "Bootloader" { /// @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) } @@ -2397,6 +2515,10 @@ object "Bootloader" { } } + function DELEGATION_BYTECODE_MARKER() -> ret { + ret := 0x0202FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + } + /// @dev Validates the transaction against the senders' account. /// Besides ensuring that the contract agrees to a transaction, /// this method also enforces that the nonce has been marked as used. @@ -2414,7 +2536,9 @@ object "Bootloader" { setHook(VM_HOOK_ACCOUNT_VALIDATION_ENTERED()) debugLog("pre-validate",0) debugLog("pre-validate",from) + let success := callAccountMethod({{VALIDATE_TX_SELECTOR}}, from, txDataOffset) + setHook(VM_HOOK_NO_VALIDATION_ENTERED()) if iszero(success) { @@ -3105,12 +3229,8 @@ 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) + debugLog("txType", txType) switch txType case 0 { let maxFeePerGas := getMaxFeePerGas(innerTxDataOffset) @@ -3139,6 +3259,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 +3286,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 +3310,36 @@ 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. + // From EIP: "The transaction is considered invalid if the length of authorization_list is zero." + assertEq(gt(getReservedDynamicBytesLength(innerTxDataOffset), 0), 1, "reservedDynamic is zero for EIP7702") } case 113 { let paymaster := getPaymaster(innerTxDataOffset) @@ -3205,6 +3357,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. @@ -3736,6 +3889,10 @@ object "Bootloader" { ret := 29 } + function FAILED_TO_PROCESS_EIP7702_DELEGATIONS_ERR_CODE() -> ret { + ret := 30 + } + /// @dev Accepts a 1-word literal and returns its length in bytes /// @param str A string literal function getStrLen(str) -> len { diff --git a/system-contracts/contracts/AccountCodeStorage.sol b/system-contracts/contracts/AccountCodeStorage.sol index 9f9a7d9a1a..a0d10b0431 100644 --- a/system-contracts/contracts/AccountCodeStorage.sol +++ b/system-contracts/contracts/AccountCodeStorage.sol @@ -56,6 +56,18 @@ contract AccountCodeStorage is IAccountCodeStorage { _storeCodeHash(_address, _hash); } + /// @notice Sets the bytecodeHash of address to indicate EIP-7702 delegation. + /// @param _address The address of the account to set the codehash to. + /// @param _hash Bytecode hash with encoded EIP-7702 delegation data. + /// @dev This method trusts the ContractDeployer to make sure that the hash is well-formed. + function storeAccount7702DelegationCodeHash(address _address, bytes32 _hash) external override onlyDeployer { + // Check that code hash corresponds to the deploying smart contract + if (!Utils.isContract7702Delegation(_hash)) { + revert InvalidCodeHash(CodeHashReason.Not7702Delegation); + } + _storeCodeHash(_address, _hash); + } + /// @notice Marks the account bytecodeHash as constructed. /// @param _address The address of the account to mark as constructed function markAccountCodeHashAsConstructed(address _address) external override onlyDeployer { diff --git a/system-contracts/contracts/BootloaderUtilities.sol b/system-contracts/contracts/BootloaderUtilities.sol index 9b0fe10bce..6cc24202dc 100644 --- a/system-contracts/contracts/BootloaderUtilities.sol +++ b/system-contracts/contracts/BootloaderUtilities.sol @@ -3,7 +3,7 @@ pragma solidity 0.8.28; 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,147 @@ 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 { + uint256 listLength = authList.length; + for (uint256 i = 0; i < listLength; ++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/ContractDeployer.sol b/system-contracts/contracts/ContractDeployer.sol index ec909f02ee..d6cf5fe779 100644 --- a/system-contracts/contracts/ContractDeployer.sol +++ b/system-contracts/contracts/ContractDeployer.sol @@ -7,10 +7,12 @@ import {IContractDeployer, ForceDeployment} from "./interfaces/IContractDeployer import {CREATE2_PREFIX, CREATE_PREFIX, NONCE_HOLDER_SYSTEM_CONTRACT, ACCOUNT_CODE_STORAGE_SYSTEM_CONTRACT, FORCE_DEPLOYER, MAX_SYSTEM_CONTRACT_ADDRESS, KNOWN_CODE_STORAGE_CONTRACT, BASE_TOKEN_SYSTEM_CONTRACT, IMMUTABLE_SIMULATOR_SYSTEM_CONTRACT, COMPLEX_UPGRADER_CONTRACT, SERVICE_CALL_PSEUDO_CALLER, EVM_PREDEPLOYS_MANAGER, EVM_HASHES_STORAGE} from "./Constants.sol"; import {Utils} from "./libraries/Utils.sol"; +import {AuthorizationListItem} from "./libraries/TransactionHelper.sol"; +import {RLPEncoder} from "./libraries/RLPEncoder.sol"; import {EfficientCall} from "./libraries/EfficientCall.sol"; import {SystemContractHelper} from "./libraries/SystemContractHelper.sol"; import {SystemContractBase} from "./abstract/SystemContractBase.sol"; -import {Unauthorized, InvalidNonceOrderingChange, ValueMismatch, EmptyBytes32, EVMBytecodeHash, EVMBytecodeHashUnknown, EVMEmulationNotSupported, NotAllowedToDeployInKernelSpace, HashIsNonZero, NonEmptyAccount, UnknownCodeHash, NonEmptyMsgValue} from "./SystemContractErrors.sol"; +import {Unauthorized, InvalidNonceOrderingChange, ValueMismatch, EmptyBytes32, EVMBytecodeHash, EVMBytecodeHashUnknown, EVMEmulationNotSupported, NotAllowedToDeployInKernelSpace, HashIsNonZero, NonEmptyAccount, UnknownCodeHash, NonEmptyMsgValue, EmptyAuthorizationList} from "./SystemContractErrors.sol"; /** * @author Matter Labs @@ -30,6 +32,19 @@ contract ContractDeployer is IContractDeployer, SystemContractBase { /// @notice What types of bytecode are allowed to be deployed on this chain. AllowedBytecodeTypes public allowedBytecodeTypesToDeploy; + /// @dev Bytecode mask for delegated accounts: + /// - Byte 0 (0x02) means the the account is processed through the EVM interpreter + /// - Byte 1 (0x02) means that the account is delegated. + /// - Bytes 2-3 (0x0017) means that the length of the bytecode is 23 bytes. + /// - Bytes 4-17 have no meaning. + /// - Bytes 9-11 (0xEF0100) are prefix for the 7702 bytecode of the contract (EF01000 || address). + /// The rest is left empty for address masking. + bytes32 private constant DELEGATION_BYTECODE_MASK = + 0x020200170000000000EF01000000000000000000000000000000000000000000; + /// @dev Mask to extract the delegation address from the bytecode hash. + bytes32 private constant DELEGATION_ADDRESS_MASK = + 0x000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; + /// @dev Restricts `msg.sender` to be this contract itself. modifier onlySelf() { if (msg.sender != address(this)) { @@ -45,6 +60,25 @@ contract ContractDeployer is IContractDeployer, SystemContractBase { return accountInfo[_address]; } + /// @notice Returns `true` if account is an EOA (including 7702-delegated ones). + /// This function will return `false` for _both_ smart contracts and smart accounts. + /// @param _address The address of the account. + /// @return `true` if the account is an EOA, `false` otherwise. + function isAccountEOA(address _address) external view returns (bool) { + bool systemContract = _address <= address(MAX_SYSTEM_CONTRACT_ADDRESS); + if (systemContract) { + return false; + } + + bool noCodeHash = ACCOUNT_CODE_STORAGE_SYSTEM_CONTRACT.getRawCodeHash(_address) == 0; + if (noCodeHash) { + return true; + } + + bool delegated = this.getAccountDelegation(_address) != address(0); + return delegated; + } + /// @notice Returns the account abstraction version if `_address` is a deployed contract. /// Returns the latest supported account abstraction version if `_address` is an EOA. /// @param _address The address of the account. @@ -56,10 +90,7 @@ 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 - ) { + if (this.isAccountEOA(_address)) { return AccountAbstractionVersion.Version1; } @@ -385,6 +416,119 @@ contract ContractDeployer is IContractDeployer, SystemContractBase { } } + /// @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) { + bytes32 codeHash = ACCOUNT_CODE_STORAGE_SYSTEM_CONTRACT.getRawCodeHash(_addr); + if (codeHash[0] == 0x02 && codeHash[1] == 0x02) { + // The first two bytes of the code hash are 0x0202, which means that the account is delegated. + // The delegation address is stored in the last 20 bytes of the code hash. + return address(uint160(uint256(codeHash & DELEGATION_ADDRESS_MASK))); + } else { + // The account is not delegated. + return address(0); + } + } + + /// @notice Method called by bootloader during processing of EIP7702 authorization lists. + /// @notice Each item is processed independently, so if any check fails for an item, + /// it is skipped and the next item is processed. + function processDelegations(AuthorizationListItem[] calldata authorizationList) external onlyCallFromBootloader { + uint256 listLength = authorizationList.length; + // The transaction is considered invalid if the length of authorization_list is zero. + require(listLength > 0, EmptyAuthorizationList()); + for (uint256 i = 0; i < listLength; ++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) + ); + bytes1 magic = bytes1(0x05); + bytes32 message = keccak256( + bytes.concat(magic, 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)); + + // We only allow delegation for EOAs. + if (!this.isAccountEOA(authority)) { + continue; + } + + // Avoid reverting if the nonce is not incremented. + (bool nonceIncremented, ) = address(NONCE_HOLDER_SYSTEM_CONTRACT).call( + abi.encodeWithSelector( + NONCE_HOLDER_SYSTEM_CONTRACT.incrementMinNonceIfEqualsFor.selector, + authority, + item.nonce + ) + ); + if (!nonceIncremented) { + continue; + } + if (item.addr == address(0)) { + bytes32 currentBytecodeHash = ACCOUNT_CODE_STORAGE_SYSTEM_CONTRACT.getRawCodeHash(authority); + ACCOUNT_CODE_STORAGE_SYSTEM_CONTRACT.storeAccount7702DelegationCodeHash(authority, 0x00); + EVM_HASHES_STORAGE.storeEvmCodeHash(currentBytecodeHash, bytes32(0x0)); + } else { + // Otherwise, store the delegation. + bytes32 delegationCodeMarker = DELEGATION_BYTECODE_MASK | bytes32(uint256(uint160(item.addr))); + ACCOUNT_CODE_STORAGE_SYSTEM_CONTRACT.storeAccount7702DelegationCodeHash( + authority, + delegationCodeMarker + ); + bytes32 evmBytecodeHash = _hash7702Delegation(delegationCodeMarker); + EVM_HASHES_STORAGE.storeEvmCodeHash(delegationCodeMarker, evmBytecodeHash); + } + } + } + + /// @notice Hashes the code part extracted from EIP-7702 delegation contract bytecode hash + /// without copying data to memory. + /// @dev This method does not check whether the input is a valid EIP-7702 delegation code hash. + /// @param input The EIP-7702 delegation code hash. + /// @return hash The keccak256 hash of the code part of the EIP-7702 delegation code hash. + function _hash7702Delegation(bytes32 input) internal pure returns (bytes32 hash) { + // Hash bytes 9-32 (that have the contract code) without allocating an array. + assembly { + // Point to free memory and store 23 bytes starting at byte offset 9 of input + let ptr := mload(0x40) + mstore(ptr, shl(72, input)) // Shift left to remove first 9 bytes (9 * 8 = 72 bits) + hash := keccak256(ptr, 23) + } + } + /// @notice Deploys a bytecode on the specified address. /// @param _bytecodeHash The correctly formatted hash of the bytecode. /// @param _newAddress The address of the contract to be deployed. diff --git a/system-contracts/contracts/EvmEmulator.yul b/system-contracts/contracts/EvmEmulator.yul index 8939ab6c29..a757d6d0b6 100644 --- a/system-contracts/contracts/EvmEmulator.yul +++ b/system-contracts/contracts/EvmEmulator.yul @@ -418,6 +418,22 @@ object "EvmEmulator" { hash := fetchFromSystemContract(ACCOUNT_CODE_STORAGE_SYSTEM_CONTRACT(), 36) } + function is7702Delegated(rawCodeHash) -> isDelegated { + isDelegated := eq(shr(240, rawCodeHash), 0x0202) + } + + function delegationAddress(rawCodeHash) -> delegationAddr { + delegationAddr := 0 + if is7702Delegated(rawCodeHash) { + // Check that there is no loop. + let storedAddr := and(rawCodeHash, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) + let delegationHash := getRawCodeHash(storedAddr) + if eq(is7702Delegated(delegationHash), 0) { + delegationAddr := storedAddr + } + } + } + function getEvmExtcodehash(versionedBytecodeHash) -> evmCodeHash { // function getEvmCodeHash(bytes32 versionedBytecodeHash) external view returns(bytes32) mstore(0, 0x5F8F27B000000000000000000000000000000000000000000000000000000000) @@ -426,9 +442,23 @@ object "EvmEmulator" { } function isHashOfConstructedEvmContract(rawCodeHash) -> isConstructedEVM { - let version := shr(248, rawCodeHash) - let isConstructedFlag := xor(shr(240, rawCodeHash), 1) - isConstructedEVM := and(eq(version, 2), isConstructedFlag) + let hashPrefix := shr(240, rawCodeHash) + switch hashPrefix + case 0x0200 { + // 0 means that account is constructed + isConstructedEVM := 1 + } + case 0x0202 { + // 2 means that account is delegated + let delegationAddress := and(rawCodeHash, 0xffffffffffffffffffffffffffffffffffffffff) + let delegationHash := getRawCodeHash(delegationAddress) + // We don't allow recursion here, since delegation loops are forbidden + isConstructedEVM := eq(shr(240, delegationHash), 0x0200) // EVM contract, constructed + } + default { + // This is not a constructed EVM contract + isConstructedEVM := 0 + } } // Basically performs an extcodecopy, while returning the length of the copied bytecode. @@ -467,6 +497,10 @@ object "EvmEmulator" { function fetchBytecode(addr) -> success, rawCodeHash { rawCodeHash := getRawCodeHash(addr) + success := $llvm_AlwaysInline_llvm$_fetchBytecodeByHash(rawCodeHash) + } + + function $llvm_AlwaysInline_llvm$_fetchBytecodeByHash(rawCodeHash) -> success { mstore(0, rawCodeHash) success := staticcall(gas(), CODE_ORACLE_SYSTEM_CONTRACT(), 0, 32, 0, 0) @@ -1454,6 +1488,7 @@ object "EvmEmulator" { offset := add(rawOffset, MEM_OFFSET()) } } + function $llvm_AlwaysInline_llvm$_calldatasize() -> size { size := 0 @@ -2030,7 +2065,11 @@ object "EvmEmulator" { } let rawCodeHash := getRawCodeHash(addr) - switch isHashOfConstructedEvmContract(rawCodeHash) + let shouldUseEvmHash := or( + is7702Delegated(rawCodeHash), + isHashOfConstructedEvmContract(rawCodeHash) + ) + switch shouldUseEvmHash case 0 { let codeLen := and(shr(224, rawCodeHash), 0xffff) @@ -3110,8 +3149,8 @@ object "EvmEmulator" { max := MAX_POSSIBLE_DEPLOYED_BYTECODE_LEN() } - function getDeployedBytecode() { - let success, rawCodeHash := fetchBytecode(getCodeAddress()) + function getDeployedBytecode(rawCodeHash) { + let success := $llvm_AlwaysInline_llvm$_fetchBytecodeByHash(rawCodeHash) let codeLen := and(shr(224, rawCodeHash), 0xffff) loadReturndataIntoActivePtr() @@ -3481,6 +3520,22 @@ object "EvmEmulator" { hash := fetchFromSystemContract(ACCOUNT_CODE_STORAGE_SYSTEM_CONTRACT(), 36) } + function is7702Delegated(rawCodeHash) -> isDelegated { + isDelegated := eq(shr(240, rawCodeHash), 0x0202) + } + + function delegationAddress(rawCodeHash) -> delegationAddr { + delegationAddr := 0 + if is7702Delegated(rawCodeHash) { + // Check that there is no loop. + let storedAddr := and(rawCodeHash, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) + let delegationHash := getRawCodeHash(storedAddr) + if eq(is7702Delegated(delegationHash), 0) { + delegationAddr := storedAddr + } + } + } + function getEvmExtcodehash(versionedBytecodeHash) -> evmCodeHash { // function getEvmCodeHash(bytes32 versionedBytecodeHash) external view returns(bytes32) mstore(0, 0x5F8F27B000000000000000000000000000000000000000000000000000000000) @@ -3489,9 +3544,23 @@ object "EvmEmulator" { } function isHashOfConstructedEvmContract(rawCodeHash) -> isConstructedEVM { - let version := shr(248, rawCodeHash) - let isConstructedFlag := xor(shr(240, rawCodeHash), 1) - isConstructedEVM := and(eq(version, 2), isConstructedFlag) + let hashPrefix := shr(240, rawCodeHash) + switch hashPrefix + case 0x0200 { + // 0 means that account is constructed + isConstructedEVM := 1 + } + case 0x0202 { + // 2 means that account is delegated + let delegationAddress := and(rawCodeHash, 0xffffffffffffffffffffffffffffffffffffffff) + let delegationHash := getRawCodeHash(delegationAddress) + // We don't allow recursion here, since delegation loops are forbidden + isConstructedEVM := eq(shr(240, delegationHash), 0x0200) // EVM contract, constructed + } + default { + // This is not a constructed EVM contract + isConstructedEVM := 0 + } } // Basically performs an extcodecopy, while returning the length of the copied bytecode. @@ -3530,6 +3599,10 @@ object "EvmEmulator" { function fetchBytecode(addr) -> success, rawCodeHash { rawCodeHash := getRawCodeHash(addr) + success := $llvm_AlwaysInline_llvm$_fetchBytecodeByHash(rawCodeHash) + } + + function $llvm_AlwaysInline_llvm$_fetchBytecodeByHash(rawCodeHash) -> success { mstore(0, rawCodeHash) success := staticcall(gas(), CODE_ORACLE_SYSTEM_CONTRACT(), 0, 32, 0, 0) @@ -4517,6 +4590,7 @@ object "EvmEmulator" { offset := add(rawOffset, MEM_OFFSET()) } } + function simulate( isCallerEVM, @@ -5081,7 +5155,11 @@ object "EvmEmulator" { } let rawCodeHash := getRawCodeHash(addr) - switch isHashOfConstructedEvmContract(rawCodeHash) + let shouldUseEvmHash := or( + is7702Delegated(rawCodeHash), + isHashOfConstructedEvmContract(rawCodeHash) + ) + switch shouldUseEvmHash case 0 { let codeLen := and(shr(224, rawCodeHash), 0xffff) @@ -6148,10 +6226,40 @@ object "EvmEmulator" { } } + function $llvm_Cold_llvm$_delegate7702( + delegationAddress, + ) -> success, returnOffset, returnLen { + returnOffset := MEM_OFFSET() + // TODO: use delegatecall by reference to avoid copying calldata + let calldataSize := calldatasize() + calldatacopy(0, 0, calldataSize) + success := delegatecall(gas(), delegationAddress, 0, calldataSize, 0, 0) + + returnLen := returndatasize() + returndatacopy(returnOffset, 0, returnLen) + } + //////////////////////////////////////////////////////////////// // FALLBACK //////////////////////////////////////////////////////////////// + let rawCodeHash := getRawCodeHash(getCodeAddress()) + let delegationAddr := delegationAddress(rawCodeHash) + if gt(delegationAddr, 0) { + // We process 7702 delegation before opening an EVM frame, + // since we don't actually perform simulation here. + // If this code is invoked from EVM interpreter, caller will + // know how to handle the result, we're only acting as a proxy. + let success, returnOffset, returnLen := $llvm_Cold_llvm$_delegate7702(delegationAddr) + switch success + case 1 { + return(returnOffset, returnLen) + } + default { + revert(returnOffset, returnLen) + } + } + let evmGasLeft, isStatic, isCallerEVM := consumeEvmFrame() if iszero(isCallerEVM) { @@ -6161,7 +6269,7 @@ object "EvmEmulator" { // First, copy the contract's bytecode to be executed into the `BYTECODE_OFFSET` // segment of memory. - getDeployedBytecode() + getDeployedBytecode(rawCodeHash) let returnOffset, returnLen := simulate(isCallerEVM, evmGasLeft, isStatic) return(returnOffset, returnLen) diff --git a/system-contracts/contracts/EvmGasManager.yul b/system-contracts/contracts/EvmGasManager.yul index 9d8f18b5a7..73e27cca8c 100644 --- a/system-contracts/contracts/EvmGasManager.yul +++ b/system-contracts/contracts/EvmGasManager.yul @@ -55,10 +55,10 @@ object "EvmGasManager" { res := verbatim_1i_1o("active_ptr_data_load", pos) } - function $llvm_AlwaysInline_llvm$__getRawSenderCodeHash() -> hash { + function $llvm_AlwaysInline_llvm$__getRawCodeHash(addr) -> hash { // function getRawCodeHash(address _address) mstore(0, 0x4DE2E46800000000000000000000000000000000000000000000000000000000) - mstore(4, caller()) + mstore(4, addr) let success := staticcall(gas(), ACCOUNT_CODE_STORAGE_SYSTEM_CONTRACT(), 0, 36, 0, 0) @@ -85,9 +85,22 @@ object "EvmGasManager" { let transientSlot := or(IS_ACCOUNT_EVM_PREFIX(), caller()) let isEVM := tload(transientSlot) if iszero(isEVM) { - let versionedCodeHash := $llvm_AlwaysInline_llvm$__getRawSenderCodeHash() + let versionedCodeHash := $llvm_AlwaysInline_llvm$__getRawCodeHash(caller()) isEVM := eq(shr(248, versionedCodeHash), 2) + // For 7702 delegations, we need to make sure that the delegation address is an EVM contract. + let isAccountDelegated := eq(and(0xFF, shr(240, versionedCodeHash)), 2) + if and(isEVM, isAccountDelegated) { + let delegationAddress := and(ADDRESS_MASK(), versionedCodeHash) + versionedCodeHash := $llvm_AlwaysInline_llvm$__getRawCodeHash(delegationAddress) + // We need to protect against delegation loops, so disallow `0x0202` as + // delegation address. + isEVM := or( + eq(shr(240, versionedCodeHash), 0x0200), + eq(shr(240, versionedCodeHash), 0x0201) + ) + } + if iszero(isEVM) { // error CallerMustBeEvmContract() mstore(0, 0xBE4BF9E400000000000000000000000000000000000000000000000000000000) diff --git a/system-contracts/contracts/NonceHolder.sol b/system-contracts/contracts/NonceHolder.sol index 45ef0b23ab..206dbaaec5 100644 --- a/system-contracts/contracts/NonceHolder.sol +++ b/system-contracts/contracts/NonceHolder.sol @@ -128,12 +128,28 @@ contract NonceHolder is INonceHolder, SystemContractBase { /// unintentionally allowing keyed nonces to be used. /// @param _expectedNonce The expected minimal nonce for the account. function incrementMinNonceIfEquals(uint256 _expectedNonce) external onlySystemCall { + _incrementMinNonceIfEqualsFor(msg.sender, _expectedNonce); + } + + /// @notice Method for `ContractDeployer` to increment the account nonce, e.g. + /// during processing of EIP-7702 authorization lists. + function incrementMinNonceIfEqualsFor(address _address, uint256 _expectedNonce) external onlySystemCall { + if (msg.sender != address(DEPLOYER_SYSTEM_CONTRACT)) { + revert Unauthorized(msg.sender); + } + _incrementMinNonceIfEqualsFor(_address, _expectedNonce); + } + + /// @notice A private method that shares logic for incrementing the minimal nonce + /// if it is equal to the `_expectedNonce`. This is used by both `incrementMinNonceIfEquals` + /// and `incrementMinNonceIfEqualsFor`. + function _incrementMinNonceIfEqualsFor(address _address, uint256 _expectedNonce) private { (uint192 nonceKey, uint64 nonceValue) = _splitKeyedNonce(_expectedNonce); if (nonceKey != 0) { revert InvalidNonceKey(nonceKey); } - uint256 addressAsKey = uint256(uint160(msg.sender)); + uint256 addressAsKey = uint256(uint160(_address)); uint256 oldRawNonce = rawNonces[addressAsKey]; (, uint256 oldMinNonce) = _splitRawNonce(oldRawNonce); diff --git a/system-contracts/contracts/SystemContractErrors.sol b/system-contracts/contracts/SystemContractErrors.sol index 6d2d01ed06..9bb629fe24 100644 --- a/system-contracts/contracts/SystemContractErrors.sol +++ b/system-contracts/contracts/SystemContractErrors.sol @@ -201,9 +201,13 @@ error InvalidNewL2BlockNumber(uint256 l2BlockNumber); // 0xe0a0dd23 error InvalidNonceKey(uint192 nonceKey); +// 0xcb494e84 +error EmptyAuthorizationList(); + enum CodeHashReason { NotContractOnConstructor, - NotConstructedContract + NotConstructedContract, + Not7702Delegation } enum SigField { diff --git a/system-contracts/contracts/interfaces/IAccountCodeStorage.sol b/system-contracts/contracts/interfaces/IAccountCodeStorage.sol index 4c8ae4ae41..b37162b79a 100644 --- a/system-contracts/contracts/interfaces/IAccountCodeStorage.sol +++ b/system-contracts/contracts/interfaces/IAccountCodeStorage.sol @@ -9,6 +9,8 @@ interface IAccountCodeStorage { function storeAccountConstructedCodeHash(address _address, bytes32 _hash) external; + function storeAccount7702DelegationCodeHash(address _address, bytes32 _hash) external; + function markAccountCodeHashAsConstructed(address _address) external; function getRawCodeHash(address _address) external view returns (bytes32 codeHash); diff --git a/system-contracts/contracts/interfaces/IContractDeployer.sol b/system-contracts/contracts/interfaces/IContractDeployer.sol index ef8831efa5..06a6b6b08d 100644 --- a/system-contracts/contracts/interfaces/IContractDeployer.sol +++ b/system-contracts/contracts/interfaces/IContractDeployer.sol @@ -208,4 +208,13 @@ interface IContractDeployer { /// @notice Changes what types of bytecodes are allowed to be deployed on the chain. /// @param newAllowedBytecodeTypes The new allowed bytecode types mode. function setAllowedBytecodeTypesToDeploy(AllowedBytecodeTypes newAllowedBytecodeTypes) external; + + /// @notice Returns the address of EIP7702 delegation for given contract. + function getAccountDelegation(address _addr) external view returns (address); + + /// @notice Returns `true` if account is an EOA (including 7702-delegated ones). + /// This function will return `false` for _both_ smart contracts and smart accounts. + /// @param _address The address of the account. + /// @return `true` if the account is an EOA, `false` otherwise. + function isAccountEOA(address _address) external view returns (bool); } diff --git a/system-contracts/contracts/interfaces/IEntryPoint.sol b/system-contracts/contracts/interfaces/IEntryPoint.sol new file mode 100644 index 0000000000..e5578713f6 --- /dev/null +++ b/system-contracts/contracts/interfaces/IEntryPoint.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +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(PackedUserOperation[] calldata _ops) external; +} diff --git a/system-contracts/contracts/interfaces/INonceHolder.sol b/system-contracts/contracts/interfaces/INonceHolder.sol index 9b884762bb..fab8e3a574 100644 --- a/system-contracts/contracts/interfaces/INonceHolder.sol +++ b/system-contracts/contracts/interfaces/INonceHolder.sol @@ -45,6 +45,10 @@ interface INonceHolder { /// @param _expectedNonce The expected minimal nonce for the account. function incrementMinNonceIfEquals(uint256 _expectedNonce) external; + /// @notice Method for `ContractDeployer` to increment the account nonce, e.g. + /// during processing of EIP-7702 authorization lists. + function incrementMinNonceIfEqualsFor(address _address, uint256 _expectedNonce) external; + /// @notice A convenience method to increment the minimal nonce if it is equal /// to the `_expectedNonce`. This is a keyed counterpart to `incrementMinNonceIfEquals`. /// @dev Reverts for nonces with nonceKey == 0. diff --git a/system-contracts/contracts/libraries/TransactionHelper.sol b/system-contracts/contracts/libraries/TransactionHelper.sol index 9eccfd93e4..9f282a53bb 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,123 @@ 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 { + uint256 listLength = authList.length; + for (uint i = 0; i < listLength; ++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/contracts/libraries/Utils.sol b/system-contracts/contracts/libraries/Utils.sol index d1ea42c88e..e9fb6a9d13 100644 --- a/system-contracts/contracts/libraries/Utils.sol +++ b/system-contracts/contracts/libraries/Utils.sol @@ -88,6 +88,13 @@ library Utils { return _bytecodeHash[1] == 0x01; } + /// @notice Denotes whether bytecode hash corresponds to an EIP-7702 delegation + function isContract7702Delegation(bytes32 _bytecodeHash) internal pure returns (bool) { + bool isEmpty = _bytecodeHash == bytes32(0); + bool is7702Delegation = _bytecodeHash[0] == 0x02 && _bytecodeHash[1] == 0x02; + return (isEmpty || is7702Delegation); + } + /// @notice Sets "isConstructor" flag to TRUE for the bytecode hash /// @param _bytecodeHash The bytecode hash for which it is needed to set the constructing flag /// @return The bytecode hash with "isConstructor" flag set to TRUE diff --git a/system-contracts/contracts/precompiles/CodeOracle.yul b/system-contracts/contracts/precompiles/CodeOracle.yul index 2cbd084ca2..205f6e13f0 100644 --- a/system-contracts/contracts/precompiles/CodeOracle.yul +++ b/system-contracts/contracts/precompiles/CodeOracle.yul @@ -130,6 +130,14 @@ object "CodeOracle" { let versionedCodeHash := calldataload(0) + if eq(shr(240, versionedCodeHash), 0x0202) { + // 7702 delegation; the bytecode is stored in the hash + // It is guaranteed to be 23 bytes long. + let bytecode := shl(72, versionedCodeHash) + mstore(0, bytecode) + return (0, 23) + } + // Can not decommit unknown code if iszero(isCodeHashKnown(versionedCodeHash)) { revert(0, 0) diff --git a/system-contracts/evm-emulator/EvmEmulator.template.yul b/system-contracts/evm-emulator/EvmEmulator.template.yul index 36d993528a..d5db8e87dc 100644 --- a/system-contracts/evm-emulator/EvmEmulator.template.yul +++ b/system-contracts/evm-emulator/EvmEmulator.template.yul @@ -109,8 +109,8 @@ object "EvmEmulator" { max := MAX_POSSIBLE_DEPLOYED_BYTECODE_LEN() } - function getDeployedBytecode() { - let success, rawCodeHash := fetchBytecode(getCodeAddress()) + function getDeployedBytecode(rawCodeHash) { + let success := $llvm_AlwaysInline_llvm$_fetchBytecodeByHash(rawCodeHash) let codeLen := and(shr(224, rawCodeHash), 0xffff) loadReturndataIntoActivePtr() @@ -143,10 +143,40 @@ object "EvmEmulator" { } } + function $llvm_Cold_llvm$_delegate7702( + delegationAddress, + ) -> success, returnOffset, returnLen { + returnOffset := MEM_OFFSET() + // TODO: use delegatecall by reference to avoid copying calldata + let calldataSize := calldatasize() + calldatacopy(0, 0, calldataSize) + success := delegatecall(gas(), delegationAddress, 0, calldataSize, 0, 0) + + returnLen := returndatasize() + returndatacopy(returnOffset, 0, returnLen) + } + //////////////////////////////////////////////////////////////// // FALLBACK //////////////////////////////////////////////////////////////// + let rawCodeHash := getRawCodeHash(getCodeAddress()) + let delegationAddr := delegationAddress(rawCodeHash) + if gt(delegationAddr, 0) { + // We process 7702 delegation before opening an EVM frame, + // since we don't actually perform simulation here. + // If this code is invoked from EVM interpreter, caller will + // know how to handle the result, we're only acting as a proxy. + let success, returnOffset, returnLen := $llvm_Cold_llvm$_delegate7702(delegationAddr) + switch success + case 1 { + return(returnOffset, returnLen) + } + default { + revert(returnOffset, returnLen) + } + } + let evmGasLeft, isStatic, isCallerEVM := consumeEvmFrame() if iszero(isCallerEVM) { @@ -156,7 +186,7 @@ object "EvmEmulator" { // First, copy the contract's bytecode to be executed into the `BYTECODE_OFFSET` // segment of memory. - getDeployedBytecode() + getDeployedBytecode(rawCodeHash) let returnOffset, returnLen := simulate(isCallerEVM, evmGasLeft, isStatic) return(returnOffset, returnLen) diff --git a/system-contracts/evm-emulator/EvmEmulatorFunctions.template.yul b/system-contracts/evm-emulator/EvmEmulatorFunctions.template.yul index bd57d72477..c331499ca8 100644 --- a/system-contracts/evm-emulator/EvmEmulatorFunctions.template.yul +++ b/system-contracts/evm-emulator/EvmEmulatorFunctions.template.yul @@ -360,6 +360,22 @@ function getRawCodeHash(addr) -> hash { hash := fetchFromSystemContract(ACCOUNT_CODE_STORAGE_SYSTEM_CONTRACT(), 36) } +function is7702Delegated(rawCodeHash) -> isDelegated { + isDelegated := eq(shr(240, rawCodeHash), 0x0202) +} + +function delegationAddress(rawCodeHash) -> delegationAddr { + delegationAddr := 0 + if is7702Delegated(rawCodeHash) { + // Check that there is no loop. + let storedAddr := and(rawCodeHash, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) + let delegationHash := getRawCodeHash(storedAddr) + if eq(is7702Delegated(delegationHash), 0) { + delegationAddr := storedAddr + } + } +} + function getEvmExtcodehash(versionedBytecodeHash) -> evmCodeHash { // function getEvmCodeHash(bytes32 versionedBytecodeHash) external view returns(bytes32) mstore(0, 0x5F8F27B000000000000000000000000000000000000000000000000000000000) @@ -368,9 +384,23 @@ function getEvmExtcodehash(versionedBytecodeHash) -> evmCodeHash { } function isHashOfConstructedEvmContract(rawCodeHash) -> isConstructedEVM { - let version := shr(248, rawCodeHash) - let isConstructedFlag := xor(shr(240, rawCodeHash), 1) - isConstructedEVM := and(eq(version, 2), isConstructedFlag) + let hashPrefix := shr(240, rawCodeHash) + switch hashPrefix + case 0x0200 { + // 0 means that account is constructed + isConstructedEVM := 1 + } + case 0x0202 { + // 2 means that account is delegated + let delegationAddress := and(rawCodeHash, 0xffffffffffffffffffffffffffffffffffffffff) + let delegationHash := getRawCodeHash(delegationAddress) + // We don't allow recursion here, since delegation loops are forbidden + isConstructedEVM := eq(shr(240, delegationHash), 0x0200) // EVM contract, constructed + } + default { + // This is not a constructed EVM contract + isConstructedEVM := 0 + } } // Basically performs an extcodecopy, while returning the length of the copied bytecode. @@ -409,6 +439,10 @@ function fetchDeployedCode(addr, dstOffset, srcOffset, len) -> copiedLen { function fetchBytecode(addr) -> success, rawCodeHash { rawCodeHash := getRawCodeHash(addr) + success := $llvm_AlwaysInline_llvm$_fetchBytecodeByHash(rawCodeHash) +} + +function $llvm_AlwaysInline_llvm$_fetchBytecodeByHash(rawCodeHash) -> success { mstore(0, rawCodeHash) success := staticcall(gas(), CODE_ORACLE_SYSTEM_CONTRACT(), 0, 32, 0, 0) @@ -1395,4 +1429,4 @@ function _genericLog(sp, stackHead, evmGasLeft, topicCount, isStatic) -> newEvmG if size { offset := add(rawOffset, MEM_OFFSET()) } -} \ No newline at end of file +} diff --git a/system-contracts/evm-emulator/EvmEmulatorLoop.template.yul b/system-contracts/evm-emulator/EvmEmulatorLoop.template.yul index 888d10ac12..bb17d8c38f 100644 --- a/system-contracts/evm-emulator/EvmEmulatorLoop.template.yul +++ b/system-contracts/evm-emulator/EvmEmulatorLoop.template.yul @@ -552,7 +552,11 @@ for { } true { } { } let rawCodeHash := getRawCodeHash(addr) - switch isHashOfConstructedEvmContract(rawCodeHash) + let shouldUseEvmHash := or( + is7702Delegated(rawCodeHash), + isHashOfConstructedEvmContract(rawCodeHash) + ) + switch shouldUseEvmHash case 0 { let codeLen := and(shr(224, rawCodeHash), 0xffff) diff --git a/system-contracts/scripts/preprocess-bootloader.ts b/system-contracts/scripts/preprocess-bootloader.ts index 29454dcd27..513fbe9046 100644 --- a/system-contracts/scripts/preprocess-bootloader.ts +++ b/system-contracts/scripts/preprocess-bootloader.ts @@ -67,6 +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_IS_ACCOUNT_EOA_SELECTOR: getPaddedSelector("ContractDeployer", "isAccountEOA"), + PROCESS_DELEGATIONS_SELECTOR: getSelector("ContractDeployer", "processDelegations"), RIGHT_PADDED_GET_RAW_CODE_HASH_SELECTOR: getPaddedSelector("AccountCodeStorage", "getRawCodeHash"), PAY_FOR_TX_SELECTOR: getSelector("DefaultAccount", "payForTransaction"), PRE_PAYMASTER_SELECTOR: getSelector("DefaultAccount", "prepareForPaymaster"),