diff --git a/l1-contracts/deploy-scripts/upgrade/SystemContractsProcessing.s.sol b/l1-contracts/deploy-scripts/upgrade/SystemContractsProcessing.s.sol index ccea2afe87..276ea137a0 100644 --- a/l1-contracts/deploy-scripts/upgrade/SystemContractsProcessing.s.sol +++ b/l1-contracts/deploy-scripts/upgrade/SystemContractsProcessing.s.sol @@ -260,6 +260,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/bootloader/bootloader.yul b/system-contracts/bootloader/bootloader.yul index 0b2861f8f8..067644c313 100644 --- a/system-contracts/bootloader/bootloader.yul +++ b/system-contracts/bootloader/bootloader.yul @@ -694,6 +694,127 @@ 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 := call( + gas(), + ACCOUNT_CODE_STORAGE_ADDR(), + 0, + 0, + 68, + 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("setRawCodeHash 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. + function getDelegationAddress(addr) -> ret { + mstore(0, {{RIGHT_PADDED_GET_ACCOUNT_DELEGATION_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) + } + + /// @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) + + // 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) + 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(), + ACCOUNT_CODE_STORAGE_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 { @@ -1465,6 +1586,8 @@ object "Bootloader" { setTxOrigin(BOOTLOADER_FORMAL_ADDR()) } + processDelegations(innerTxDataOffset) + success := executeL2Tx(txDataOffset, from) if isNotEnoughGasForPubdata( @@ -1910,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, @@ -1924,10 +2080,20 @@ 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) + 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) @@ -1941,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 @@ -2152,28 +2328,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) } @@ -2414,7 +2610,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) { @@ -2546,7 +2756,20 @@ 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 delegation := getDelegationAddress(from) + + switch delegation + case 0 { + // Account not delegated: invoke the `execute` method + ret := callAccountMethod({{EXECUTE_TX_SELECTOR}}, from, txDataOffset) + } + default { + // Account is delegated: invoke through mimicall using calldata provided + let innerTxDataOffset := add(txDataOffset, 32) + let calldataPtr := getDataPtr(innerTxDataOffset) + let value := getValue(innerTxDataOffset) + ret := msgValueSimulatorMimicCall(from, from, value, calldataPtr) + } if iszero(ret) { debugReturndata() @@ -3105,12 +3328,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 +3358,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 +3385,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 +3409,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 +3455,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 9f9a7d9a1a..b40bde2a9e 100644 --- a/system-contracts/contracts/AccountCodeStorage.sol +++ b/system-contracts/contracts/AccountCodeStorage.sol @@ -3,10 +3,18 @@ pragma solidity 0.8.28; 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 @@ -20,9 +28,13 @@ 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. + mapping(address => address) private delegatedEOAs; + modifier onlyDeployer() { if (msg.sender != address(DEPLOYER_SYSTEM_CONTRACT)) { revert Unauthorized(msg.sender); @@ -153,4 +165,104 @@ contract AccountCodeStorage is IAccountCodeStorage { bytes32 bytecodeHash = getRawCodeHash(_addr); return Utils.isCodeHashEVM(bytecodeHash); } + + /// @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]; + } + + /// @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, + // 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.getRawCodeHash(authority) != 0x00 && this.getAccountDelegation(authority) == address(0)) { + 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]; + _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); + } + } + } + + // 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); + } } diff --git a/system-contracts/contracts/BootloaderUtilities.sol b/system-contracts/contracts/BootloaderUtilities.sol index 9b0fe10bce..75b9978d5e 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,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/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/contracts/erc4337/EntryPointV01.sol b/system-contracts/contracts/erc4337/EntryPointV01.sol new file mode 100644 index 0000000000..ac879bdf5d --- /dev/null +++ b/system-contracts/contracts/erc4337/EntryPointV01.sol @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +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"; +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 *; + + 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 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? + + _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/IAccountCodeStorage.sol b/system-contracts/contracts/interfaces/IAccountCodeStorage.sol index 4c8ae4ae41..1172294f15 100644 --- a/system-contracts/contracts/interfaces/IAccountCodeStorage.sol +++ b/system-contracts/contracts/interfaces/IAccountCodeStorage.sol @@ -18,4 +18,6 @@ interface IAccountCodeStorage { function getCodeSize(uint256 _input) external view returns (uint256 codeSize); function isAccountEVM(address _addr) external view returns (bool); + + function getAccountDelegation(address _addr) external view returns (address); } 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/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/constants.ts b/system-contracts/scripts/constants.ts index decd88e963..34b899dbb0 100644 --- a/system-contracts/scripts/constants.ts +++ b/system-contracts/scripts/constants.ts @@ -193,6 +193,12 @@ export const SYSTEM_CONTRACTS: ISystemContracts = { lang: Language.Solidity, location: SourceLocation.SystemContracts, }, + entryPoint: { + address: "0x0000000000000000000000000000000000008016", + codeName: "EntryPoint", + lang: Language.Solidity, + location: SourceLocation.SystemContracts, + }, create2Factory: { // This is explicitly a non-system-contract address. // We do not use the same address as create2 factories on EVM, since diff --git a/system-contracts/scripts/preprocess-bootloader.ts b/system-contracts/scripts/preprocess-bootloader.ts index 29454dcd27..eb48ba3a02 100644 --- a/system-contracts/scripts/preprocess-bootloader.ts +++ b/system-contracts/scripts/preprocess-bootloader.ts @@ -68,6 +68,9 @@ 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_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"), VALIDATE_AND_PAY_PAYMASTER: getSelector("IPaymaster", "validateAndPayForPaymasterTransaction"),