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

Draft
wants to merge 12 commits into
base: draft-v28
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
279 changes: 265 additions & 14 deletions system-contracts/bootloader/bootloader.yul

Large diffs are not rendered by default.

116 changes: 114 additions & 2 deletions system-contracts/contracts/AccountCodeStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Check failure on line 7 in system-contracts/contracts/AccountCodeStorage.sol

View workflow job for this annotation

GitHub Actions / lint

imported name SystemContractsCaller is not used

Check failure on line 7 in system-contracts/contracts/AccountCodeStorage.sol

View workflow job for this annotation

GitHub Actions / lint

imported name SystemContractsCaller is not used

Check failure on line 7 in system-contracts/contracts/AccountCodeStorage.sol

View workflow job for this annotation

GitHub Actions / lint

imported name SystemContractsCaller is not used
import {EfficientCall} from "./libraries/EfficientCall.sol";
import {Transaction, AuthorizationListItem} from "./libraries/TransactionHelper.sol";

Check failure on line 9 in system-contracts/contracts/AccountCodeStorage.sol

View workflow job for this annotation

GitHub Actions / lint

imported name Transaction is not used

Check failure on line 9 in system-contracts/contracts/AccountCodeStorage.sol

View workflow job for this annotation

GitHub Actions / lint

imported name Transaction is not used

Check failure on line 9 in system-contracts/contracts/AccountCodeStorage.sol

View workflow job for this annotation

GitHub Actions / lint

imported name Transaction is not used
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 [email protected]
Expand All @@ -20,9 +28,13 @@
* 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;
Copy link
Collaborator

@StanislavBreadless StanislavBreadless May 15, 2025

Choose a reason for hiding this comment

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

just in case, dont store anything in this contract except for code hashes for accounts, the role of code hash registry is used on the vm level and bad things will happen if unintended value is stored for some key


modifier onlyDeployer() {
if (msg.sender != address(DEPLOYER_SYSTEM_CONTRACT)) {
revert Unauthorized(msg.sender);
Expand Down Expand Up @@ -153,4 +165,104 @@
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++) {

Check failure on line 183 in system-contracts/contracts/AccountCodeStorage.sol

View workflow job for this annotation

GitHub Actions / lint

GC: Found [ .length ] property in Loop condition. Suggestion: assign it to a variable

Check failure on line 183 in system-contracts/contracts/AccountCodeStorage.sol

View workflow job for this annotation

GitHub Actions / lint

GC: For [ i ] variable, increment/decrement by 1 using: [ ++variable ] to save gas

Check failure on line 183 in system-contracts/contracts/AccountCodeStorage.sol

View workflow job for this annotation

GitHub Actions / lint

GC: Found [ .length ] property in Loop condition. Suggestion: assign it to a variable

Check failure on line 183 in system-contracts/contracts/AccountCodeStorage.sol

View workflow job for this annotation

GitHub Actions / lint

GC: For [ i ] variable, increment/decrement by 1 using: [ ++variable ] to save gas

Check failure on line 183 in system-contracts/contracts/AccountCodeStorage.sol

View workflow job for this annotation

GitHub Actions / lint

GC: Found [ .length ] property in Loop condition. Suggestion: assign it to a variable

Check failure on line 183 in system-contracts/contracts/AccountCodeStorage.sol

View workflow job for this annotation

GitHub Actions / lint

GC: For [ i ] variable, increment/decrement by 1 using: [ ++variable ] to save gas
// 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)

Check failure on line 207 in system-contracts/contracts/AccountCodeStorage.sol

View workflow job for this annotation

GitHub Actions / lint

Named parameters missing. MIN unnamed argumenst is 4

Check failure on line 207 in system-contracts/contracts/AccountCodeStorage.sol

View workflow job for this annotation

GitHub Actions / lint

Named parameters missing. MIN unnamed argumenst is 4

Check failure on line 207 in system-contracts/contracts/AccountCodeStorage.sol

View workflow job for this annotation

GitHub Actions / lint

Named parameters missing. MIN unnamed argumenst is 4
);

// 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;
}
Comment on lines +210 to +221
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 part was copy-pasted from some other place, needs to be checked.


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)) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

this. implies an external call to itself, it is better to avoid using this when it is not required for cheaper costs

continue;
}

bool nonceIncremented = this._performRawMimicCall(

Check failure on line 230 in system-contracts/contracts/AccountCodeStorage.sol

View workflow job for this annotation

GitHub Actions / lint

Named parameters missing. MIN unnamed argumenst is 4

Check failure on line 230 in system-contracts/contracts/AccountCodeStorage.sol

View workflow job for this annotation

GitHub Actions / lint

Named parameters missing. MIN unnamed argumenst is 4

Check failure on line 230 in system-contracts/contracts/AccountCodeStorage.sol

View workflow job for this annotation

GitHub Actions / lint

Named parameters missing. MIN unnamed argumenst is 4
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?
Copy link
Collaborator

Choose a reason for hiding this comment

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

Here the semantics that you imply is not "I delegate to this address", but more like "I use the same bytecode as another address".

It has the following differences from EIP7702:

  • In EIP7702 extcodesize/extcodehash dont return the exact bytecode info. This is not really an important issue
  • In EIP7702 in case of delegation loops (i.e. default account pointing to another account), this wont work, but in your case it will.
  • There is a (potential) use-case of delegating to an address that has not yet been deployed, e.g. an account may want to initialize its implementation as a first-ever transaction. Not sure how important is it. It does not work in your case, since the bytecode copied is 0.

I would personally make it more like EIP7702: reset DefaultAccount to some default code that has another delegation as an immutable. Note, that this implementation will still have to check that another delegator is not a delegator.

But my approach has drawbacks:

  • It is is just harder to implement
  • In EIP7702 no actual delegatecall is done, but in my case it will be.
  • It will be more expensive since before executing the call I need to retrieve whether another address is a delegator.

So yeah.. not sure

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);

Check failure on line 266 in system-contracts/contracts/AccountCodeStorage.sol

View workflow job for this annotation

GitHub Actions / lint

Named parameters missing. MIN unnamed argumenst is 4

Check failure on line 266 in system-contracts/contracts/AccountCodeStorage.sol

View workflow job for this annotation

GitHub Actions / lint

Named parameters missing. MIN unnamed argumenst is 4

Check failure on line 266 in system-contracts/contracts/AccountCodeStorage.sol

View workflow job for this annotation

GitHub Actions / lint

Named parameters missing. MIN unnamed argumenst is 4
}
}
146 changes: 145 additions & 1 deletion system-contracts/contracts/BootloaderUtilities.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -34,6 +34,8 @@
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);
}
Expand Down Expand Up @@ -339,4 +341,146 @@
)
);
}

/// @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++) {

Check failure on line 403 in system-contracts/contracts/BootloaderUtilities.sol

View workflow job for this annotation

GitHub Actions / lint

GC: Found [ .length ] property in Loop condition. Suggestion: assign it to a variable

Check failure on line 403 in system-contracts/contracts/BootloaderUtilities.sol

View workflow job for this annotation

GitHub Actions / lint

Rule is set with explicit type [var/s: uint]

Check failure on line 403 in system-contracts/contracts/BootloaderUtilities.sol

View workflow job for this annotation

GitHub Actions / lint

GC: For [ i ] variable, increment/decrement by 1 using: [ ++variable ] to save gas

Check failure on line 403 in system-contracts/contracts/BootloaderUtilities.sol

View workflow job for this annotation

GitHub Actions / lint

GC: Found [ .length ] property in Loop condition. Suggestion: assign it to a variable

Check failure on line 403 in system-contracts/contracts/BootloaderUtilities.sol

View workflow job for this annotation

GitHub Actions / lint

Rule is set with explicit type [var/s: uint]

Check failure on line 403 in system-contracts/contracts/BootloaderUtilities.sol

View workflow job for this annotation

GitHub Actions / lint

GC: For [ i ] variable, increment/decrement by 1 using: [ ++variable ] to save gas

Check failure on line 403 in system-contracts/contracts/BootloaderUtilities.sol

View workflow job for this annotation

GitHub Actions / lint

GC: Found [ .length ] property in Loop condition. Suggestion: assign it to a variable

Check failure on line 403 in system-contracts/contracts/BootloaderUtilities.sol

View workflow job for this annotation

GitHub Actions / lint

Rule is set with explicit type [var/s: uint]

Check failure on line 403 in system-contracts/contracts/BootloaderUtilities.sol

View workflow job for this annotation

GitHub Actions / lint

GC: For [ i ] variable, increment/decrement by 1 using: [ ++variable ] to save gas
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
)
);
}
}
8 changes: 4 additions & 4 deletions system-contracts/contracts/ContractDeployer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,10 @@
}

// 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;
}

Expand Down Expand Up @@ -168,7 +168,7 @@
function createEVM(bytes calldata _initCode) external payable override onlySystemCall returns (uint256, address) {
uint256 senderNonce;
// If the account is an EOA, use the min nonce. If it's a contract, use deployment nonce
if (msg.sender == tx.origin) {

Check warning on line 171 in system-contracts/contracts/ContractDeployer.sol

View workflow job for this annotation

GitHub Actions / lint

Avoid to use tx.origin

Check warning on line 171 in system-contracts/contracts/ContractDeployer.sol

View workflow job for this annotation

GitHub Actions / lint

Avoid to use tx.origin

Check warning on line 171 in system-contracts/contracts/ContractDeployer.sol

View workflow job for this annotation

GitHub Actions / lint

Avoid to use tx.origin
// Subtract 1 for EOA since the nonce has already been incremented for this transaction
senderNonce = NONCE_HOLDER_SYSTEM_CONTRACT.getMinNonce(msg.sender) - 1;
} else {
Expand Down
Loading
Loading