1 of 84

AA Workshop:

Account

What are the required functionalities for an Account contract? What to keep in mind when designing your own Account?

Dec 19 2023 | Dapp Learning

Equal access to the Tokenized World

2 of 84

Nic Lin

imToken Labs

3 of 84

What are the required functionalities for an Account?

I will mainly use 4337 examples but most of them apply to native AA too. I will mention the difference if there’s any.

What to keep in mind when designing your own Account?

4 of 84

What are the required functionalities for an Account?

  1. Pay for the fee!
  2. Validation!
  3. Deployment

5 of 84

How to pay for your transaction in 4337?

6 of 84

How to pay for your transaction in 4337?

  • Transaction payment is made in ETH, just like normal ETH transactions

User

Bundler

ETH

ERC20

Proposer

ETH

7 of 84

How to pay for your transaction in 4337?

Transaction payment is made in ETH, just like normal ETH transactions

    • But you can not use `approve`/`transferFrom` with ETH
    • So user has to deposit ETH to Entrypoint and pay Entrypoint with the deposited ETH

User

Bundler

ETH

Entrypoint

ETH

8 of 84

How to pay for your transaction in 4337?

  • User can deposit via
    • transfer ETH directly to Entrypoint
    • or call `Entrypoint.depositTo(address account)`

User

Entrypoint

ETH

depositTo(User)

ETH

9 of 84

How to pay for your transaction in 4337?

  • During validation, Entrypoint will calculate total gas fee required:
    • `maxFeePerGas * (callGasLimit + verificationGasLimit + preVerificationGas) `

User

Gas price I’m willing to pay

Max gas for my execution

Max gas for validating my transaction

Gas for Entrypoint processing overhead

10 of 84

How to pay for your transaction in 4337?

  • During validation, Entrypoint will calculate total gas fee required:
    • `maxFeePerGas * (callGasLimit + verificationGasLimit + preVerificationGas)`
  • If `balanceOf(user)` not enough, Entrypoint will pass the amount short to user’s Account contract via `validateUserOp`
    • `validateUserOp(..., missingAccountFunds)`
    • User will have to deposit enough ETH during `validateUserOp`

Entrypoint

User

User’s Account contract

validateUserOp(...)

ETH

Your deposit not enough, please deposit

11 of 84

How to pay for your transaction in 4337?

Entrypoint

User

User’s Account contract

validateUserOp(...)

ETH

Your deposit not enough, please deposit

Bundler

This is my UserOp, I’m willing to pay��`maxFeePerGas *(callGasLimit + verificationGasLimit + preVerificationGas)` ��ETH for this op.

12 of 84

How to pay for your transaction in 4337?

  • What about payment in ERC-20?

13 of 84

How to pay for your transaction in 4337?

  • What about payment in ERC-20?
    • Need the help of Paymaster

User

Bundler

ETH

ERC20

Proposer

ETH

Paymaster

ETH

14 of 84

How to pay for your transaction in 4337?

15 of 84

How to validate your transaction in 4337?

16 of 84

How to validate your transaction in 4337?

  • Entrypoint calls `validateUserOp` function on user’s Account contract
    • If the call succeed and returns `0`, validation passed
    • If the call reverted or returns `1`, validation failed

Entrypoint

User’s Account contract

validateUserOp(...)

returns 0

User authorized 👍

Entrypoint

User’s Account contract

validateUserOp(...)

returns 1 or revert

User did NOT authorize 👎

17 of 84

How to validate your transaction in 4337?

  • `validateUserOp(userOp, userOpHash, missingAccountFunds) `
    • User’s Account contract should verify `userOp.signature` against `userOpHash`
      • User can sign and verify using any signing algorithm

Entrypoint

User’s Account contract

validateUserOp(...)

returns 0

Signature checked.

User authorized 👍

ECDSA

18 of 84

How to validate your transaction in 4337?

  • If validation succeed, Entrypoint calls user’s Account contract again to execute what user want to execute

Entrypoint

User’s Account contract

validateUserOp(...)

returns 0

User authorized 👍

execute(...)

19 of 84

How to validate your transaction in 4337?

  • IMPORTANT: you need to validate caller in your execute function
    • DO NOT assume only Entrypoint will call your Account contract

Entrypoint

User’s Account contract

execute(...)

Entrypoint want me to execute, it must have called validateUserOp already

swap

20 of 84

How to validate your transaction in 4337?

  • IMPORTANT: you need to validate caller in your execute function
    • DO NOT assume only Entrypoint will call your Account contract

User’s Account contract

execute(...)

You are not Entrypoint

21 of 84

How to validate your transaction in 4337?

22 of 84

How to deploy your Account contract in 4337?

23 of 84

How to deploy your Account contract in 4337?

  1. User pre-deploys the Account contract himself
  2. Entrypoint deploys user’s Account contract when user executes his first transaction

24 of 84

How to deploy your Account contract in 4337?

  • User pre-deploys the Account contract himself
  • Entrypoint deploys user’s Account contract when user executes his first transaction

25 of 84

How to deploy your Account contract in 4337?

  • Entrypoint deploys user’s Account contract when user executes his first transaction
    • User needs to inform Entrypoint of deployment information

26 of 84

How to deploy your Account contract in 4337?

  • Entrypoint deploys user’s Account contract when user executes his first transaction
    • User needs to inform Entrypoint of deployment information
      • If user does not inform and user’s contract has not been deployed
        • Transaction failure

Entrypoint

User

User’s Account contract

Please execute my UserOp

check contract exists

Not exist

27 of 84

How to deploy your Account contract in 4337?

  • Entrypoint deploys user’s Account contract when user executes his first transaction
    • User needs to inform Entrypoint of deployment information
      • If user does not inform and user’s contract has not been deployed
        • Transaction failure
      • If user provide deployment information while user’s contract is already deployed
        • Transaction failure

Entrypoint

User

Please deploy my Account contract and execute my UserOp

deploy

Already exist

User’s Account contract

28 of 84

How to deploy your Account contract in 4337?

  • Entrypoint deploys user’s Account contract when user executes his first transaction
    • User needs to inform Entrypoint of deployment information
      • Factory of user’s Account contract and calldata for the factory

Entrypoint

User

Please deploy my Account contract, this is the factory address and data to call the factory for my contract deployment

Factory of user’s Account contract

29 of 84

How to deploy your Account contract in 4337?

  • Entrypoint deploys user’s Account contract when user executes his first transaction
    • User needs to inform Entrypoint of deployment information
      • Factory of user’s Account contract and calldata for the factory

User

In the initcode field of my UserOp:

UserOp {

initcode: [20 bytes factory address][calldata for factory]

}

30 of 84

How to deploy your Account contract in 4337?

  • Entrypoint deploys user’s Account contract when user executes his first transaction
    • User needs to inform Entrypoint of deployment information
      • Factory of user’s Account contract and calldata for the factory

UserOp {

initcode: [20 bytes factory address][calldata for factory]

}

For example:

ABI encode of “createAccount(owner,salt)”

31 of 84

How to deploy your Account contract in 4337?

  • Entrypoint deploys user’s Account contract when user executes his first transaction
    • User needs to inform Entrypoint of deployment information
      • If deployed contract address does not match the sender in transaction
        • Transaction failure

Entrypoint

Bob

Please deploy my Account contract and execute my UserOp

deploy

This is not Bob’s Account contract

Alice’s Account contract

32 of 84

How to deploy your Account contract in 4337?

  • Entrypoint deploys user’s Account contract when user executes his first transaction
    • User needs to inform Entrypoint of deployment information
      • If deployed contract address does not match the sender in transaction
        • Transaction failure
      • User needs to precompute his Account contract address using factory
        • set this address as the sender in transaction

Bob

Where will my contract be deployed at?

Factory of Bob’s Account contract

getAddress(owner,salt)

33 of 84

How to deploy your Account contract in 4337?

Bob

Where will my contract be deployed at?

Factory of Bob’s Account contract

getAddress(owner,salt)

0x1234beef

34 of 84

How to deploy your Account contract in 4337?

Entrypoint

Bob

Please deploy my Account contract and execute my UserOp

deploy

UserOp {

sender: 0x1234beef

initcode: …

}

User’s Account contract

Factory of Bob’s Account contract

deploy

35 of 84

How to deploy your Account contract in 4337?

36 of 84

What are the required functionalities for an Account?

  • Pay for the fee!
  • Validation!
  • Deployment

37 of 84

What are the required functionalities for an Account?

  • Pay for the fee!
  • Validation!
  • Deployment

ETH

ECDSA

=> same as an EOA

e.g., zkSync’s Default Account

38 of 84

What are the required functionalities for an Account?

  • Pay for the fee!
  • Validation!
  • Deployment

ETH

ECDSA

=> same as an EOA

e.g., zkSync’s Default Account

Native AA does not support deploy at first transaction :(

39 of 84

What are the required functionalities for an Account?

  • Pay for the fee!
  • Validation!
  • Deployment

ETH

ECDSA

=> same as an EOA

e.g., zkSync’s Default Account

Native AA does not support deploy at first transaction :(

Either user needs a deploy transaction or wallet/dApp hides it from UX

e.g., wallet/dApp deploys for user and charges for deployment fee at user’s first transaction

40 of 84

What to keep in mind when designing your own Account?

41 of 84

  • Add in more features to your Account
  • Safety concerns
  • Debugging tips

What to keep in mind when designing your own Account?

42 of 84

  • Add in more features to your Account
    1. 4337 opcodes & storage access restrictions
    2. 4337 nonce mechanism
    3. Difference between 4337 & native AA
    4. Modular design
  • Safety considerations
  • Debugging tips

What to keep in mind when designing your own Account?

43 of 84

Add in more features to your Account

  • 4337 opcodes & storage access restrictions
    • To prevent DoS attack toward bundler

44 of 84

Add in more features to your Account

  • 4337 opcodes & storage access restrictions
    • To prevent DoS attack toward bundler
    • These are soft restrictions
      • You can still send userOp on-chain yourself and bypass all restrictions
      • Each bundler can have different subset of these restrictions
    • These restrictions may change over time

45 of 84

Add in more features to your Account

  • 4337 opcodes & storage access restrictions
    • https://eips.ethereum.org/EIPS/eip-4337#simulation
    • During `validateUserOp`
      • No reading ETH balances
      • No `tx.origin`, `timestamp` or `gasleft`

46 of 84

Add in more features to your Account

  • 4337 opcodes & storage access restrictions
    • https://eips.ethereum.org/EIPS/eip-4337#simulation
    • During `validateUserOp`
      • No reading ETH balances
      • No `tx.origin`, `timestamp` or `gasleft`
      • Can not use Openzeppelin’s `safeTransfer` of SafeERC20
        • because it uses Address library to make function call
        • and Address library checks self balance before making function call 😅

47 of 84

Add in more features to your Account

  • 4337 opcodes & storage access restrictions
    • https://eips.ethereum.org/EIPS/eip-4337#simulation
    • During `validateUserOp`
      • No reading ETH balances
      • No `tx.origin`, `timestamp` or `gasleft`
      • Can not use Openzeppelin’s `safeTransfer` of SafeERC20
        • because it uses Address library to make function call
        • and Address library checks self balance before making function call 😅
      • Can not access storage unrelated to Account contract
        • related: `mapping(address => uint256) balanceOf`
          • but it can not be a nested mapping:
            • `mapping(address => mapping(uint256 => MyStruct)`

48 of 84

Add in more features to your Account

  • 4337 opcodes & storage access restrictions
    • https://eips.ethereum.org/EIPS/eip-4337#simulation
    • During `validateUserOp`
      • No reading ETH balances
      • No `tx.origin`, `timestamp` or `gasleft`
      • Can not use Openzeppelin’s `safeTransfer` of SafeERC20
        • because it uses Address library to make function call
        • and Address library checks self balance before making function call 😅
      • Can not access storage unrelated to Account contract
        • related: `mapping(address => uint256) balanceOf`
          • but it can not be a nested mapping:
            • ❌ `mapping(address => mapping(uint256 => MyStruct)`
      • Can not call an upgradeable contract, e.g., USDC
        • because in an upgradeable contract, proxy contract stores its implementation contract address in its storage (unrelated to Account contract)
        • there’s proposal to use whitelist to bypass this

49 of 84

Add in more features to your Account

  • 4337 opcodes & storage access restrictions
    • 4337 performs validation of all userOps before execution of all userOps

50 of 84

Add in more features to your Account

  • 4337 opcodes & storage access restrictions
    • 4337 performs validation of all userOps before execution of all userOps
    • Avoid recording userOp infos and execute based on the recorded infos during `validateUserOp`
    • Avoid accessing and recording from same state across your different userOps
      • e.g., taking number from a ticket machine and record the number during `validateUserOp` and execute based on the number recorded

51 of 84

Add in more features to your Account

  • 4337 nonce mechanism
    • 4337 separates 256 bits nonce into 192 bits `key` and 64 bits `nonce`
    • `nonce` is only 64 bits and stored using a mapping indexed by `key` :
      • `mapping(address => mapping(uint192 => uint256)) public nonceSequenceNumber;`

52 of 84

Add in more features to your Account

  • 4337 nonce mechanism
    • 4337 separates 256 bits nonce into 192 bits `key` and 64 bits `nonce`
    • `nonce` is only 64 bits and stored using a mapping indexed by `key` :
      • `mapping(address => mapping(uint192 => uint256)) public nonceSequenceNumber;`
    • Default to `key` being `0` but remember `nonce` is only 64 bits
      • larger than 64 bits is a different `nonce`
    • Simply deploying the Account contract also increments it’s `nonce`

53 of 84

Add in more features to your Account

  • Difference between 4337 & native AA
    • Entry function of validation phase is fixed in 4337 & Native AA
      • 4337: `validateUserOp`
      • zkSync: `validateTransaction`
      • StarkNet: `__validate__`
    • Entry function of execution phase is fixed in Native AA but not 4337
      • zkSync: `executeTransaction`
      • StarkNet: `__execute__`
      • 4337: specified by user

54 of 84

Add in more features to your Account

  • Difference between 4337 & native AA
    • Entry function of validation phase is fixed in 4337 & Native AA
      • 4337: `validateUserOp`
      • zkSync: `validateTransaction`
      • StarkNet: `__validate__`
    • Entry function of execution phase is fixed in Native AA but not 4337
      • zkSync: `executeTransaction`
      • StarkNet: `__execute__`
      • 4337: specified by user
      • So in Native AA, it takes extra steps if you are executing Account’s own function, e.g., `transferOwnership`
      • you need to route the function call in e.g., `__execute__`
        • `if (to == address(this)) { … }`

55 of 84

Add in more features to your Account

  • Difference between 4337 & native AA
    • Entry function of validation phase is fixed in 4337 & Native AA
      • 4337: `validateUserOp`
      • zkSync: `validateTransaction`
      • StarkNet: `__validate__`
    • Entry function of execution phase is fixed in Native AA but not 4337
      • zkSync: `executeTransaction`
      • StarkNet: `__execute__`
      • 4337: specified by user
      • So in Native AA, it takes extra steps if you are executing Account’s own function, e.g., `transferOwnership`
      • you need to route the function call in e.g., `__execute__`
        • `if (to == address(this)) { … }`
        • or simply execute each calls regardless of the call content
          • If it calls itself, then it means it’s executing Account’s own functions
          • then you need to guard Account’s own functions with e.g., `assert_only_self`

56 of 84

Add in more features to your Account

  • Difference between 4337 & native AA
    • Execution flow is different between 4337 & Native AA, design accordingly
      • ERC-6900 is incompatible with Native AA
    • Protect Account’s own functions with `assert_only_self` !

57 of 84

Add in more features to your Account

  • Difference between 4337 & native AA
    • In Native AA, Account can access transaction info during execution phase
      • In zkSync: `txHash` and full transaction data is passed into `executeTransaction`
      • In StarkNet: Account can query transaction info via system call `get_tx_info`

58 of 84

Add in more features to your Account

  • Difference between 4337 & native AA
    • In Native AA, Account can access transaction info during execution phase
      • In zkSync: `txHash` and full transaction data is passed into `executeTransaction`
      • In StarkNet: Account can query transaction info via system call `get_tx_info`
    • But in 4337, Account can NOT access userOp infos during execution phase
      • you can only get it in `validateUserOp`
        • maybe record it during `validateUserOp` ? but remember there could be multiple userOps in a single batch so latter userOp will overwrite former one

59 of 84

Add in more features to your Account

  • Difference between 4337 & native AA
    • In Native AA, Account can access transaction info during execution phase
      • In zkSync: `txHash` and full transaction data is passed into `executeTransaction`
      • In StarkNet: Account can query transaction info via system call `get_tx_info`
    • But in 4337, Account can NOT access userOp infos during execution phase
      • you can only get it in `validateUserOp`
        • maybe record it during `validateUserOp` ? but remember there could be multiple userOps in a single batch so latter userOp will overwrite former one
    • 4337 devs are aware of the need to access userOp infos during execution phase
      • Adding `executeUserOp` to Entrypoint
        • `executeUserOp(UserOperation userOp, bytes32 userOpHash)`
        • Likely to be included in version 0.7.0

60 of 84

Add in more features to your Account

  • Difference between 4337 & native AA
    • Native AAs have different opcodes & storage access restrictions
      • zkSync:
        • Account can access storage slot of its address on other contracts
          • e.g., Bob’s Account deployed at `0xabc`
          • Bob’s Account can access storage slot `0xabc` on other contracts
        • Could allow accessing `timestamp` to enable transaction expiry in the future
      • StarkNet
        • Ban ALL external calls, i.e., Account can only access its own storage

61 of 84

Add in more features to your Account

  • Difference between 4337 & native AA
    • Different nonce mechanism
      • 4337: splits into 192 bits `key` and 64 bits `nonce`
      • zkSync: single incrementing 256 bits `nonce`
        • may become more flexible in the future
      • StarkNet: NO nonce abstraction
        • protocol maintains a `nonce` for each address, just like Ethereum
        • zkSync maintains nonce in a `NonceHolder` system contract

62 of 84

Add in more features to your Account

  • Modular design
    • ERC-6900
    • ERC-7579
    • Modules

63 of 84

Safety considerations

64 of 84

Safety considerations

  • Avoid `delegatecall`
    • You will have to spend extra work off-chain to prevent modules from tempering with critical storage in your Account
  • Structured storage over unstructured one
    • To prevent storage collision in complex contract inheritance or upgrading contract
    • Or at least sort and display all (unstructed) storage slots in a contract to help viewer

65 of 84

Safety considerations

  • Multi-chain deployment
    • Users nowadays assume their account address are the same across different chains
      • it is true for EOA, but becomes trickier for contract
    • Wintermute losed 20m OP token because they assume their Safe multisig address is the same across different chains

66 of 84

Safety considerations

  • Multi-chain deployment
    • Users nowadays assume their account address are the same across different chains
      • it is true for EOA, but becomes trickier for contract
    • Wintermute losed 20m OP token because they assume their Safe multisig address is the same across different chains
    • Safe don’t provide this guarantee and ask users not to assume Safe address is the same across different chains
      • they use ERC-3770 to help user better identify their Safe address
        • `eth:0x7e…E4c9`
        • `poly:0xA0b…eB48`
      • and working on ENS to address this challenge
        • `alice.safe.eth` resolves to `eth:0x7e…`, `poly:0xA0b…` etc.

67 of 84

Safety considerations

  • Multi-chain deployment
    • You need to prevent attacker front-run your Account deployment and take control of your Account on other chain

68 of 84

Safety considerations

  • Multi-chain deployment
    • You need to prevent attacker front-run your Account deployment and take control of your Account on other chain
      • You need to encode (initial) owner info into Account contract deployment data
        • Encode into `salt` so different owner settings result in different Account address
        • Same owner info will be used to initialize Account
      • If attacker front-run your Account deployment, the deployed Account is still under your control

69 of 84

Safety considerations

  • Multi-chain deployment
    • Challenge: user can NOT forget the initial owner private key
      • otherwise he will lost control over all his Accounts that are not yet being deployed

70 of 84

Safety considerations

  • Multi-chain deployment
    • Challenge: user can NOT forget the initial owner private key
      • otherwise he will lost control over all his Accounts that are not yet being deployed
    • Possible solution: centralized deployment, i.e., only privileged entity can deploy
      • Privileged entity can specify owner info for each deployment
        • but need to trust the privileged entity

71 of 84

Safety considerations

  • Multi-chain deployment
    • Challenge: user can NOT forget the initial owner private key
      • otherwise he will lost control over all his Accounts that are not yet being deployed
    • Possible solution: centralized deployment, i.e., only privileged entity can deploy
      • Privileged entity can specify owner info for each deployment
        • but need to trust the privileged entity
    • Idea solution: ENS
      • `arbitrum.alice.eth`, `optimism.alice.eth` etc.

72 of 84

Debugging tips (4337)

73 of 84

Debugging tips (4337)

  • 4337 is not Native AA, so it’s different from debugging an Ethereum transaction
    • more complicated, like debugging a contract

74 of 84

Debugging tips (4337)

  • 4337 is not Native AA, so it’s different from debugging an Ethereum transaction
    • more complicated, like debugging a contract
  • Entrypoint handles error using Custom Error instead of revert string
    • revert string: `require(condition, “error string”)`
    • Custom Error: `error FailedOp(uint256 opIndex, string reason)`

75 of 84

Debugging tips (4337)

  • 4337 is not Native AA, so it’s different from debugging an Ethereum transaction
    • more complicated, like debugging a contract
  • Entrypoint handles error using Custom Error instead of revert string
    • revert string: `require(condition, “”error string”)`
    • Custom Error: `error FailedOp(uint256 opIndex, string reason)`
    • If you simulate userOp and fail, you will get encoded Custom Error message:
      • For example, if you provide a wrong `nonce` in your userOp, Entrypoint will `revert FailedOp(opIndex, “AA25 invalid account nonce”)`
        • but you will not get error message like “FailedOp(blabla…)”

76 of 84

Debugging tips (4337)

  • 4337 is not Native AA, so it’s different from debugging an Ethereum transaction
    • more complicated, like debugging a contract
  • Entrypoint handles error using Custom Error instead of revert string
    • revert string: `require(condition, “”error string”)`
    • Custom Error: `error FailedOp(uint256 opIndex, string reason)`
    • If you simulate userOp and fail, you will get encoded Custom Error message:
      • For example, if you provide a wrong `nonce` in your userOp, Entrypoint will `revert FailedOp(opIndex, “AA25 invalid account nonce”)`
        • but you will not get error message like “FailedOp(blabla…)”
        • you will get `0x220266b600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001a4141323520696e76616c6964206163636f756e74206e6f6e6365000000000000`
        • if you decode it with `cast 4bd`:
          • `"FailedOp(uint256,string)"`
          • `0` & `AA25 invalid account nonce`

77 of 84

Debugging tips (4337)

  • Entrypoint handles error using Custom Error instead of revert string
    • There are also other Custom Errors defined
    • You need to decode the error to know what went wrong
    • If you are using Foundry and expect to trigger Entrypoint error in your test:
      • `vm.expectRevert(abi.encodeWithSelector(IEntryPoint.FailedOp.selector, ...))`

78 of 84

Debugging tips (4337)

  • If userOp failed on-chain
    • If it failed during validation phase
      • Fortunately Etherscan supports Custom Error so you can see the error clear

79 of 84

Debugging tips (4337)

  • If userOp failed on-chain
    • If it failed during validation phase
      • Fortunately Etherscan supports Custom Error so you can see the error clear
    • If it failed during execution phase
      • No Custom Error
      • Only `UserOperationEvent` is emitted
        • `UserOperationEvent` is emitted for every userOp sent on-chain

80 of 84

Debugging tips (4337)

  • If userOp failed on-chain
    • If it failed during validation phase
      • Fortunately Etherscan supports Custom Error so you can see the error clear
    • If it failed during execution phase
      • No Custom Error
      • Only `UserOperationEvent` is emitted
        • `UserOperationEvent` is emitted for every userOp sent on-chain
    • Maybe use a 4337 Explorer?
      • Validation phase error example 🤔
      • Execution phase error example 🤔

81 of 84

  • Add in more features to your Account
    • Beware of restrictions but don’t limit your self to it
    • Beware of difference between 4337 & Native AA
  • Safety considerations
  • Debugging tips

What to keep in mind when designing your own Account?

82 of 84

  • Add in more features to your Account
  • Safety considerations
    • Avoid `delegatecall`
    • Plan for multi-chain deployment
  • Debugging tips

What to keep in mind when designing your own Account?

83 of 84

  • Add in more features to your Account
  • Safety considerations
  • Debugging tips
    • Need to decode Custom Error
    • Validation phase error and Execution phase error are displayed differently

What to keep in mind when designing your own Account?

84 of 84

Thank you

Apr 24 2023 | POPOP Taipei

Equal access to the Tokenized World