Skip to content

[not for merge] (heavily wip) EIP4337 & EIP7702 #1438

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 164 additions & 7 deletions system-contracts/bootloader/bootloader.yul
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these checks should be included inside validateTypedTxStructure, if for the other types the reserverd dynamic is not zero, they should revert

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checks in validateTypedTxStructure are already in place, this logic is present here because we call this method for any transaction

let isDelegationProvided := gt(length, 0)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

must not be zero:

"The transaction is considered invalid if the length of authorization_list is zero."

https://github.com/ethereum/EIPs/blob/master/EIPS/eip-7702.md

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check is also present in validateTypedTxStructure, here it's just a sanity check to avoid invoking method with empty calldata.

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 {
Expand Down Expand Up @@ -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
) {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
}
}

Expand All @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

may need to do vibe check in the community for it, though I dont have strong opinions

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We discussed it within team, and decided that probably it's better to allow for now: even if we want to deprecate native AA, we still have chains like Sophon that rely on paymasters, and forbidding paymaster usage would mean that 7702 won't really work on Sophon.

// TOOD: and native accounts have their own entrypoint.
success := callAccountMethod({{VALIDATE_AND_PAY_PAYMASTER}}, paymaster, txDataOffset)
}

Expand Down Expand Up @@ -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.
Expand All @@ -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) {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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")
Expand All @@ -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")

<!-- @if BOOTLOADER_TYPE!='playground_batch' -->

let from := getFrom(innerTxDataOffset)
let iseoa := isEOA(from)
assertEq(iseoa, true, "Only EIP-712 can use non-EOA")

<!-- @endif -->

<!-- @if BOOTLOADER_TYPE=='proved_batch' -->
assertEq(gt(getFrom(innerTxDataOffset), MAX_SYSTEM_CONTRACT_ADDR()), 1, "from in kernel space")
<!-- @endif -->

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)
Expand All @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down
12 changes: 12 additions & 0 deletions system-contracts/contracts/AccountCodeStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading
Loading