├── .github └── workflows │ └── test.yml ├── .gitignore ├── .gitmodules ├── Makefile ├── README.md ├── foundry.toml ├── img ├── ethereum │ ├── account-abstraction-2.excalidraw │ ├── account-abstraction-again.png │ ├── account-abstraction.excalidraw │ ├── account-abstraction.png │ ├── account-abstraction.svg │ └── traditional-transaction.png └── zkSync │ ├── account-abstraction.excalidraw │ ├── account-abstraction.png │ └── account-abstraction.svg ├── javascript-scripts ├── DeployZkMinimal.ts ├── EncryptKey.ts └── SendAATx.ts ├── package.json ├── script ├── DeployMinimal.s.sol ├── HelperConfig.s.sol └── SendPackedUserOp.s.sol ├── src ├── ethereum │ └── MinimalAccount.sol └── zksync │ └── ZkMinimalAccount.sol ├── test ├── ethereum │ └── MinimalAccountTest.t.sol └── zksync │ └── ZkMinimalAccountTest.t.sol └── yarn.lock /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: workflow_dispatch 4 | 5 | env: 6 | FOUNDRY_PROFILE: ci 7 | 8 | jobs: 9 | check: 10 | strategy: 11 | fail-fast: true 12 | 13 | name: Foundry project 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | submodules: recursive 19 | 20 | - name: Install Foundry 21 | uses: foundry-rs/foundry-toolchain@v1 22 | with: 23 | version: nightly 24 | 25 | - name: Run Forge build 26 | run: | 27 | forge --version 28 | forge build --sizes 29 | id: build 30 | 31 | - name: Run Forge tests 32 | run: | 33 | forge test -vvv 34 | id: test 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | !/broadcast 7 | /broadcast/*/31337/ 8 | /broadcast/**/dry-run/ 9 | 10 | # Docs 11 | docs/ 12 | 13 | # Dotenv file 14 | .env 15 | 16 | # patrick 17 | zkout/ 18 | broadcast/ 19 | .notes.md 20 | debugger/ 21 | .vscode 22 | .DS_Store 23 | .encryptedKey.json 24 | .gitignore 25 | .devex_issues.md 26 | 27 | # Node.js 28 | node_modules/ 29 | npm-debug.log 30 | yarn-error.log 31 | .pnp/ 32 | .pnp.js -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | [submodule "lib/account-abstraction"] 5 | path = lib/account-abstraction 6 | url = https://github.com/eth-infinitism/account-abstraction 7 | [submodule "lib/openzeppelin-contracts"] 8 | path = lib/openzeppelin-contracts 9 | url = https://github.com/openzeppelin/openzeppelin-contracts 10 | [submodule "lib/foundry-era-contracts"] 11 | path = lib/foundry-era-contracts 12 | url = https://github.com/Cyfrin/foundry-era-contracts 13 | [submodule "lib/foundry-devops"] 14 | path = lib/foundry-devops 15 | url = https://github.com/cyfrin/foundry-devops 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | -include .env 2 | 3 | .PHONY: all test clean deploy fund help install snapshot format anvil scopefile flatten encryptKey 4 | 5 | DEFAULT_ANVIL_KEY := 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 6 | DEFAULT_ZKSYNC_LOCAL_KEY := 0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110 7 | CONTRACT_DEPLOYER_MAINNET := 0x0000000000000000000000000000000000008006 8 | 9 | all: remove install build 10 | 11 | # Clean the repo 12 | clean :; forge clean 13 | 14 | # Remove modules 15 | remove :; rm -rf .gitmodules && rm -rf .git/modules/* && rm -rf lib && touch .gitmodules && git add . && git commit -m "modules" 16 | 17 | install :; forge install foundry-rs/forge-std@v1.8.2 --no-commit && forge install openzeppelin/openzeppelin-contracts@v5.0.2 --no-commit && forge install eth-infinitism/account-abstraction@v0.7.0 --no-commit && forge install cyfrin/foundry-era-contracts@0.0.3 --no-commit && forge install cyfrin/foundry-devops@0.2.2 --no-commit 18 | 19 | # Update Dependencies 20 | update:; forge update 21 | 22 | format :; forge fmt 23 | 24 | anvil :; anvil -m 'test test test test test test test test test test test junk' --steps-tracing --block-time 1 25 | 26 | slither :; slither . --config-file slither.config.json 27 | 28 | aderyn :; aderyn . 29 | 30 | scope :; tree ./src/ | sed 's/└/#/g; s/──/--/g; s/├/#/g; s/│ /|/g; s/│/|/g' 31 | 32 | scopefile :; @tree ./src/ | sed 's/└/#/g' | awk -F '── ' '!/\.sol$$/ { path[int((length($$0) - length($$2))/2)] = $$2; next } { p = "src"; for(i=2; i<=int((length($$0) - length($$2))/2); i++) if (path[i] != "") p = p "/" path[i]; print p "/" $$2; }' > scope.txt 33 | 34 | # /*////////////////////////////////////////////////////////////// 35 | # EVM 36 | # //////////////////////////////////////////////////////////////*/ 37 | build:; forge build 38 | 39 | test :; forge test 40 | 41 | testFork :; forge test --fork-url mainnet 42 | 43 | snapshot :; forge snapshot 44 | 45 | # /*////////////////////////////////////////////////////////////// 46 | # EVM - SCRIPTS 47 | # //////////////////////////////////////////////////////////////*/ 48 | # How we got the mock entrypoint contract so quick 49 | getEntryPoint :; forge clone -c 1 --etherscan-api-key ${ETHERSCAN_API_KEY} 0x0000000071727De22E5E9d8BAf0edAc6f37da032 --no-git 50 | 51 | flattenClone :; forge flatten src/core/EntryPoint.sol > MockEntryPoint.sol 52 | 53 | deployEth :; forge script script/DeployMinimal.s.sol --rpc-url arbitrum --sender ${SMALL_MONEY_SENDER} --account smallmoney --broadcast --verify -vvvv 54 | 55 | verify :; forge verify-contract --etherscan-api-key ${ETHERSCAN_API_KEY} --rpc-url ${MAINNET_RPC_URL} XXX 56 | 57 | getCalldata :; cast calldata "approve(address,uint256)" 0x9EA9b0cc1919def1A3CfAEF4F7A66eE3c36F86fC 100000000000000000000 58 | 59 | estimate :; cast estimate "approve(address,uint256)" "approve(address,uint256)" 0x9EA9b0cc1919def1A3CfAEF4F7A66eE3c36F86fC 100000000000000000000 60 | 61 | sendUserOp :; forge script script/SendPackedUserOp.s.sol --rpc-url arbitrum --sender ${SMALL_MONEY_SENDER} --account smallmoney --broadcast -vvvv 62 | 63 | # /*////////////////////////////////////////////////////////////// 64 | # ZKSYNC 65 | # //////////////////////////////////////////////////////////////*/ 66 | zkbuild:; foundryup-zksync && forge build --zksync && foundryup 67 | 68 | zktest :; foundryup-zksync && forge test --zksync --system-mode=true && foundryup 69 | 70 | # /*////////////////////////////////////////////////////////////// 71 | # ZKSYNC -SCRIPTS 72 | # //////////////////////////////////////////////////////////////*/ 73 | zkanvil :; npx zksync-cli dev start 74 | 75 | flatten :; tail -n +4 <(forge flatten src/zkSync/ZkMinimalAccount.sol) > ZkMinimalFlat.sol 76 | 77 | encryptKey :; yarn encryptKey 78 | 79 | zkdeploy :; yarn deploy 80 | 81 | sendTx :; yarn sendTx -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!IMPORTANT] 2 | > This repo is for demo purposes only. 3 | 4 | # Account Abstraction 5 | 6 |
7 |

8 | aa 9 |

10 |
11 | 12 | - [Account Abstraction](#account-abstraction) 13 | - [What is Account Abstraction?](#what-is-account-abstraction) 14 | - [What's this repo show?](#whats-this-repo-show) 15 | - [What does this repo not show?](#what-does-this-repo-not-show) 16 | - [Getting Started](#getting-started) 17 | - [Requirements](#requirements) 18 | - [Installation](#installation) 19 | - [Quickstart](#quickstart) 20 | - [Vanilla Foundry](#vanilla-foundry) 21 | - [Deploy - Arbitrum](#deploy---arbitrum) 22 | - [User operation - Arbitrum](#user-operation---arbitrum) 23 | - [zkSync Foundry](#zksync-foundry) 24 | - [Deploy - zkSync local network](#deploy---zksync-local-network) 25 | - [Additional Requirements](#additional-requirements) 26 | - [Setup - local node](#setup---local-node) 27 | - [Deploy - local node](#deploy---local-node) 28 | - [Deploy - zkSync Sepolia or Mainnet](#deploy---zksync-sepolia-or-mainnet) 29 | - [Example Deployments](#example-deployments) 30 | - [zkSync (Sepolia)](#zksync-sepolia) 31 | - [Ethereum (Arbitrum)](#ethereum-arbitrum) 32 | - [Account Abstraction zkSync Contract Deployment Flow](#account-abstraction-zksync-contract-deployment-flow) 33 | - [First time](#first-time) 34 | - [Subsequent times](#subsequent-times) 35 | - [FAQ](#faq) 36 | - [What if I don't add the contract hash to factory deps?](#what-if-i-dont-add-the-contract-hash-to-factory-deps) 37 | - [Why can't we do these deployments with foundry or cast?](#why-cant-we-do-these-deployments-with-foundry-or-cast) 38 | - [Why can I use `forge create --legacy` to deploy a regular contract?](#why-can-i-use-forge-create---legacy-to-deploy-a-regular-contract) 39 | - [Acknowledgements](#acknowledgements) 40 | - [Disclaimer](#disclaimer) 41 | 42 | ## What is Account Abstraction? 43 | 44 | EoAs are now smart contracts. That's all account abstraction is. 45 | 46 | But what does that mean? 47 | 48 | Right now, every single transaction in web3 stems from a single private key. 49 | 50 | > account abstraction means that not only the execution of a transaction can be arbitrarily complex computation logic as specified by the EVM, but also the authorization logic. 51 | 52 | - [Vitalik Buterin](https://ethereum-magicians.org/t/implementing-account-abstraction-as-part-of-eth1-x/4020) 53 | - [EntryPoint Contract v0.6](https://etherscan.io/address/0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789) 54 | - [EntryPoint Contract v0.7](https://etherscan.io/address/0x0000000071727De22E5E9d8BAf0edAc6f37da032) 55 | - [zkSync AA Transaction Flow](https://docs.zksync.io/build/developer-reference/account-abstraction.html#the-transaction-flow) 56 | 57 | ## What's this repo show? 58 | 59 | 1. A minimal EVM "Smart Wallet" using alt-mempool AA 60 | 1. We even send a transactoin to the `EntryPoint.sol` 61 | 2. A minimal zkSync "Smart Wallet" using native AA 62 | 1. [zkSync uses native AA, which is slightly different than ERC-4337](https://docs.zksync.io/build/developer-reference/account-abstraction.html#iaccount-interface) 63 | 2. We *do* send our zkSync transaction to the alt-mempool 64 | 65 | ## What does this repo not show? 66 | 67 | 1. Sending your userop to the alt-mempool 68 | 1. You can learn how to do this via the [alchemy docs](https://alchemy.com/?a=673c802981) 69 | 70 | # Getting Started 71 | 72 | ## Requirements 73 | 74 | - [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) 75 | - You'll know you did it right if you can run `git --version` and you see a response like `git version x.x.x` 76 | - [foundry](https://getfoundry.sh/) 77 | - You'll know you did it right if you can run `forge --version` and you see a response like `forge 0.2.0 (816e00b 2023-03-16T00:05:26.396218Z)` 78 | - [foundry-zksync](https://github.com/matter-labs/foundry-zksync) 79 | - You'll know you did it right if you can run `forge-zksync --help` and you see `zksync` somewhere in the output 80 | 81 | ## Installation 82 | 83 | ```bash 84 | git clone https://github.com/PatrickAlphaC/minimal-account-abstraction 85 | cd minimal-account-abstraction 86 | make 87 | ``` 88 | 89 | # Quickstart 90 | 91 | ## Vanilla Foundry 92 | 93 | ```bash 94 | foundryup 95 | make test 96 | ``` 97 | 98 | ### Deploy - Arbitrum 99 | 100 | ```bash 101 | make deployEth 102 | ``` 103 | 104 | ### User operation - Arbitrum 105 | 106 | ```bash 107 | make sendUserOp 108 | ``` 109 | 110 | ## zkSync Foundry 111 | 112 | ```bash 113 | foundryup-zksync 114 | make zkbuild 115 | make zktest 116 | ``` 117 | 118 | ### Deploy - zkSync local network 119 | 120 | #### Additional Requirements 121 | - [npx & npm](https://docs.npmjs.com/cli/v10/commands/npm-install) 122 | - You'll know you did it right if you can run `npm --version` and you see a response like `7.24.0` and `npx --version` and you see a response like `8.1.0`. 123 | - [yarn](https://classic.yarnpkg.com/lang/en/docs/install/#mac-stable) 124 | - You'll know you did it right if you can run `yarn --version` and you see a response like `1.22.17`. 125 | - [docker](https://docs.docker.com/engine/install/) 126 | - You'll know you did it right if you can run `docker --version` and you see a response like `Docker version 20.10.7, build f0df350`. 127 | - Then, you'll want the daemon running, you'll know it's running if you can run `docker --info` and in the output you'll see something like the following to know it's running: 128 | ```bash 129 | Client: 130 | Context: default 131 | Debug Mode: false 132 | ``` 133 | 134 | Install dependencies: 135 | ```bash 136 | yarn 137 | ``` 138 | 139 | #### Setup - local node 140 | 141 | ```bash 142 | # Select `in memory node` and nothing else 143 | npx zksync-cli dev start 144 | ``` 145 | 146 | #### Deploy - local node 147 | 148 | > [!IMPORTANT] 149 | > *Never* have a private key associated with real funds in plaintext. 150 | 151 | ```bash 152 | # Setup your .env file, see the .env.example for an example 153 | make zkdeploy 154 | ``` 155 | 156 | > Note: Sending an account abstraction transaction doesn't work on the local network, because we don't have the system contracts setup on the local network. 157 | 158 | ### Deploy - zkSync Sepolia or Mainnet 159 | 160 | Make sure your wallet has at least 0.01 zkSync ETH in it. 161 | 162 | 1. Encrypt your key 163 | 164 | Add your `PRIVATE_KEY` and `PRIVATE_KEY_PASSWORD` to your `.env` file, then run: 165 | 166 | ```bash 167 | make encryptKey 168 | ``` 169 | 170 | > [!IMPORTANT] 171 | > NOW DELETE YOUR PRIVATE KEY AND PASSWORD FROM YOUR `.env` FILE!!! 172 | > Don't push your `.encryptedKey.json` up to GitHub either! 173 | 174 | 1. Un-Comment the Sepolia or Mainnet section (depending on which you'd like to use) of `DeployZkMinimal.ts` and `SendAATx.ts`: 175 | 176 | ```javascript 177 | // // Sepolia - uncomment to use 178 | ``` 179 | 180 | 3. Deploy the contract 181 | ```bash 182 | make zkdeploy 183 | ``` 184 | 185 | You'll get an output like: 186 | ``` 187 | zkMinimalAccount deployed to: 0x4768d649Da9927a8b3842108117eC0ca7Bc6953f 188 | With transaction hash: 0x103f6d894c20620dc632896799960d06ca37e722d20682ca824d428579ba157c 189 | ``` 190 | 191 | Grab the address of the `zkMinimalAccount` and add it to the `ZK_MINIMAL_ADDRESS` of `SendAATx.ts`. 192 | 193 | 4. Fund your account 194 | 195 | Send it `0.002` zkSync sepolia ETH. 196 | 197 | 5. Send an AA transaction 198 | 199 | ```bash 200 | make sendTx 201 | ``` 202 | 203 | You'll get an out put like this: 204 | 205 | ``` 206 | Let's do this! 207 | Setting up contract details... 208 | The owner of this minimal account is: 0x643315C9Be056cDEA171F4e7b2222a4ddaB9F88D 209 | Populating transaction... 210 | Signing transaction... 211 | The minimal account nonce before the first tx is 0 212 | Transaction sent from minimal account with hash 0xec7800e3a01d5ba5e472396127b656f7058cdcc5a1bd292b2b49f76aa19548c8 213 | The account's nonce after the first tx is 1 214 | ``` 215 | 216 | # Example Deployments 217 | 218 | ## zkSync (Sepolia) 219 | - [ZkMinimal Account (Sepolia)](https://sepolia.explorer.zksync.io/address/0xCB38Bdc1527c3F69E13701328546cA6FE23C5691) 220 | - [USDC Approval via native zkSync AA (Sepolia)](https://sepolia.explorer.zksync.io/tx/0x43224b566a0b7497a26c57ab0fcea7d033dccd6cd6e16004523be0ce14fbd0fd) 221 | - [Contract Deployer](https://explorer.zksync.io/address/0x0000000000000000000000000000000000008006) 222 | 223 | ## Ethereum (Arbitrum) 224 | - [Minimal Account](https://arbiscan.io/address/0x03Ad95a54f02A40180D45D76789C448024145aaF#code) 225 | - [USDC Approval via EntryPoint](https://arbiscan.io/tx/0x03f99078176ace63d36c5d7119f9f1c8a74da61516616c43593162ff34d1154b#eventlog) 226 | 227 | # Account Abstraction zkSync Contract Deployment Flow 228 | 229 | ## First time 230 | 1. Calls `createAccount` or `create2Account` on the `CONTRACT_DEPLOYER` system contract 231 | 1. This will deploy the contract *to the L1*. 232 | 2. Mark the contract hash in the `KnownCodesStorage` contract 233 | 3. Mark it as an AA contract 234 | 4. [Example](https://sepolia.explorer.zksync.io/tx/0xec0d587903415b2785d542f8b41c21b82ad0613c226a8c83376ec2b8f90ffdd0#eventlog) 235 | 1. Notice 6 logs emitted? 236 | 237 | ## Subsequent times 238 | 1. Calls `createAccount` or `create2Account` on the `CONTRACT_DEPLOYER` system contract 239 | 1. The `CONTRACT_DEPLOYER` will check and see it's deployed this hash before 240 | 2. It will put in another system contract that this address is associated with the first has 241 | 3. [Example](https://sepolia.explorer.zksync.io/tx/0xe7a2a895d9854db5a6cc60df60524852d9957dd17adcc5720749f60b4da3eba7) 242 | 1. Only 3 logs emitted! 243 | 244 | # FAQ 245 | 246 | ## What if I don't add the contract hash to factory deps? 247 | The transaction will revert. The `ContractDeployer` checks to see if it knows the hash, and if not, it will revert! The `ContractDeployer` calls the `KnownCodesStorage` contract, which keeps track of *every single contract hash deployed on the zkSync chain. Crazy right!* 248 | 249 | ## Why can't we do these deployments with foundry or cast? 250 | Foundry and cast don't have support for the `factoryDeps` transaction field, or support for type `113` transactions. 251 | 252 | ## Why can I use `forge create --legacy` to deploy a regular contract? 253 | `foundry-zksync` is smart enough to see a legacy deployment (when you send a transaction to the 0 address with data) and transform it into a contract call to the deployer. It's only smart enough for legacy deployments as of today, not the new `EIP-1559` type 2 transactions or account creation. 254 | 255 | # Acknowledgements 256 | - [Types of AAs on different chains](https://www.bundlebear.com/factories/all) 257 | - [eth-infinitism](https://github.com/eth-infinitism/account-abstraction/) 258 | - [Dan Nolan](https://www.youtube.com/watch?v=b4KWkIAPa3U) 259 | - [Twitter Video](https://x.com/BeingDanNolan/status/1795848790043218029) 260 | - [zerodevapp](https://github.com/zerodevapp/kernel/) 261 | - [Alchemy LightAccount](https://github.com/alchemyplatform/light-account/) 262 | 263 | # Disclaimer 264 | *This codebase is for educational purposes only and has not undergone a security review.* -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = "src" 3 | out = "out" 4 | libs = ["lib"] 5 | remappings = ['@openzeppelin/contracts=lib/openzeppelin-contracts/contracts'] 6 | is-system = true 7 | via-ir = true 8 | fs_permissions = [ 9 | { access = "read", path = "./broadcast" }, 10 | { access = "read", path = "./reports" }, 11 | ] 12 | 13 | # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options 14 | -------------------------------------------------------------------------------- /img/ethereum/account-abstraction-again.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cyfrin/minimal-account-abstraction/e185c7d6ad2dbbe746e669c2f16c629c94276d95/img/ethereum/account-abstraction-again.png -------------------------------------------------------------------------------- /img/ethereum/account-abstraction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cyfrin/minimal-account-abstraction/e185c7d6ad2dbbe746e669c2f16c629c94276d95/img/ethereum/account-abstraction.png -------------------------------------------------------------------------------- /img/ethereum/traditional-transaction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cyfrin/minimal-account-abstraction/e185c7d6ad2dbbe746e669c2f16c629c94276d95/img/ethereum/traditional-transaction.png -------------------------------------------------------------------------------- /img/zkSync/account-abstraction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cyfrin/minimal-account-abstraction/e185c7d6ad2dbbe746e669c2f16c629c94276d95/img/zkSync/account-abstraction.png -------------------------------------------------------------------------------- /javascript-scripts/DeployZkMinimal.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs-extra" 2 | import { utils, Wallet, Provider, EIP712Signer, types, Contract, ContractFactory } from "zksync-ethers" 3 | import * as ethers from "ethers" 4 | import "dotenv/config" 5 | 6 | async function main() { 7 | // Local net - comment to unuse 8 | // let provider = new Provider("http://127.0.0.1:8011") 9 | // let wallet = new Wallet(process.env.PRIVATE_KEY!) 10 | 11 | // Sepolia - uncomment to use 12 | let provider = new Provider(process.env.ZKSYNC_SEPOLIA_RPC_URL!) 13 | const encryptedJson = fs.readFileSync(".encryptedKey.json", "utf8") 14 | let wallet = Wallet.fromEncryptedJsonSync( 15 | encryptedJson, 16 | process.env.PRIVATE_KEY_PASSWORD! 17 | ) 18 | 19 | // // Mainnet - uncomment to use 20 | // let provider = new Provider(process.env.ZKSYNC_RPC_URL!) 21 | // const encryptedJson = fs.readFileSync(".encryptedKey.json", "utf8") 22 | // let wallet = Wallet.fromEncryptedJsonSync( 23 | // encryptedJson, 24 | // process.env.PRIVATE_KEY_PASSWORD! 25 | // ) 26 | 27 | wallet = wallet.connect(provider) 28 | console.log(`Working with wallet: ${await wallet.getAddress()}`) 29 | const abi = JSON.parse(fs.readFileSync("./out/ZkMinimalAccount.sol/ZkMinimalAccount.json", "utf8"))["abi"] 30 | const bytecode = JSON.parse(fs.readFileSync("./zkout/ZkMinimalAccount.sol/ZkMinimalAccount.json", "utf8"))["bytecode"]["object"] 31 | 32 | const factoryDeps = [bytecode] // We can skip this, but this is what's happening 33 | const zkMinimalAccountFactory = new ContractFactory( 34 | abi, 35 | bytecode, 36 | wallet, 37 | "createAccount", 38 | ) 39 | 40 | // const deployOptions = { 41 | // customData: { 42 | // salt: ethers.ZeroHash, 43 | // // What if we don't do factoryDeps? 44 | // // factoryDeps, 45 | // // factoryDeps: factoryDeps 46 | // // Ah! The ContractFactory automatically adds it in! 47 | // }, 48 | // } 49 | 50 | const zkMinimalAccount = await zkMinimalAccountFactory.deploy() 51 | 52 | // The above should send the following calldata: 53 | // 0xecf95b8a0000000000000000000000000000000000000000000000000000000000000000010006ddf1eae1b53a0a62fab1fc8b4fd95c8a6f4d5fe540bf109f17bae0a431000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000 54 | // 55 | // If you pop it into `calldata-decode` you'd see that the inputs to the `createAccount` are correct 56 | // cast calldata-decode "createAccount(bytes32,bytes32,bytes,uint8)" 57 | 58 | console.log(`zkMinimalAccount deployed to: ${await zkMinimalAccount.getAddress()}`) 59 | console.log(`With transaction hash: ${(await zkMinimalAccount.deploymentTransaction())!.hash}`) 60 | } 61 | 62 | main() 63 | .then(() => process.exit(0)) 64 | .catch((error) => { 65 | console.error(error) 66 | process.exit(1) 67 | }) -------------------------------------------------------------------------------- /javascript-scripts/EncryptKey.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers" 2 | import * as fs from "fs-extra" 3 | import "dotenv/config" 4 | 5 | async function main() { 6 | const wallet = new ethers.Wallet(process.env.PRIVATE_KEY!) 7 | const encryptedJsonKey = await wallet.encrypt( 8 | process.env.PRIVATE_KEY_PASSWORD!, 9 | ) 10 | fs.writeFileSync("./.encryptedKey.json", encryptedJsonKey) 11 | } 12 | 13 | main() 14 | .then(() => process.exit(0)) 15 | .catch((error) => { 16 | console.error(error) 17 | process.exit(1) 18 | }) -------------------------------------------------------------------------------- /javascript-scripts/SendAATx.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs-extra" 2 | import { utils, Wallet, Provider, EIP712Signer, types, Contract, ContractFactory } from "zksync-ethers" 3 | import * as ethers from "ethers" 4 | import "dotenv/config" 5 | 6 | // Mainnet 7 | // const ZK_MINIMAL_ADDRESS = "" 8 | 9 | // Sepolia 10 | // const ZK_MINIMAL_ADDRESS = "" 11 | 12 | // Local 13 | // Update this! 14 | const ZK_MINIMAL_ADDRESS = "0x19a519025994A1F32188dE1F0E11014A791fB358" 15 | 16 | // Update this too! 17 | const RANDOM_APPROVER = "0x9EA9b0cc1919def1A3CfAEF4F7A66eE3c36F86fC" 18 | 19 | // Mainnet 20 | // const USDC_ZKSYNC = "0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4" 21 | // Sepolia 22 | const USDC_ZKSYNC = "0x5249Fd99f1C1aE9B04C65427257Fc3B8cD976620" 23 | 24 | // Local 25 | // let USDC_ZKSYNC = "" 26 | 27 | const AMOUNT_TO_APPROVE = "1000000" 28 | 29 | async function main() { 30 | console.log("Let's do this!") 31 | 32 | // Local net 33 | // let provider = new Provider("http://127.0.0.1:8011") 34 | // let wallet = new Wallet(process.env.PRIVATE_KEY!) 35 | 36 | // // Sepolia - Uncomment to use 37 | let provider = new Provider(process.env.ZKSYNC_SEPOLIA_RPC_URL!) 38 | const encryptedJson = fs.readFileSync(".encryptedKey.json", "utf8") 39 | let wallet = Wallet.fromEncryptedJsonSync( 40 | encryptedJson, 41 | process.env.PRIVATE_KEY_PASSWORD! 42 | ) 43 | 44 | // // Mainnet - Uncomment to use 45 | // let provider = new Provider(process.env.ZKSYNC_RPC_URL!) 46 | // const encryptedJson = fs.readFileSync(".encryptedKey.json", "utf8") 47 | // let wallet = Wallet.fromEncryptedJsonSync( 48 | // encryptedJson, 49 | // process.env.PRIVATE_KEY_PASSWORD! 50 | // ) 51 | 52 | wallet = wallet.connect(provider) 53 | 54 | const abi = JSON.parse(fs.readFileSync("./out/ZkMinimalAccount.sol/ZkMinimalAccount.json", "utf8"))["abi"] 55 | console.log("Setting up contract details...") 56 | const zkMinimalAccount = new Contract(ZK_MINIMAL_ADDRESS, abi, provider) 57 | 58 | // If this doesn't log the owner, you have an issue! 59 | console.log(`The owner of this minimal account is: `, await zkMinimalAccount.owner()) 60 | const usdcAbi = JSON.parse(fs.readFileSync("./out/ERC20/IERC20.sol/IERC20.json", "utf8"))["abi"] 61 | const usdcContract = new Contract(USDC_ZKSYNC, usdcAbi, provider) 62 | 63 | console.log("Populating transaction...") 64 | let approvalData = await usdcContract.approve.populateTransaction( 65 | RANDOM_APPROVER, 66 | AMOUNT_TO_APPROVE 67 | ) 68 | 69 | let aaTx = approvalData 70 | 71 | const gasLimit = await provider.estimateGas({ 72 | ...aaTx, 73 | from: wallet.address, 74 | }) 75 | const gasPrice = (await provider.getFeeData()).gasPrice! 76 | 77 | aaTx = { 78 | ...aaTx, 79 | from: ZK_MINIMAL_ADDRESS, 80 | gasLimit: gasLimit, 81 | gasPrice: gasPrice, 82 | chainId: (await provider.getNetwork()).chainId, 83 | nonce: await provider.getTransactionCount(ZK_MINIMAL_ADDRESS), 84 | type: 113, 85 | customData: { 86 | gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT, 87 | } as types.Eip712Meta, 88 | value: 0n, 89 | } 90 | const signedTxHash = EIP712Signer.getSignedDigest(aaTx) 91 | 92 | console.log("Signing transaction...") 93 | const signature = ethers.concat([ 94 | ethers.Signature.from(wallet.signingKey.sign(signedTxHash)).serialized, 95 | ]) 96 | console.log(signature) 97 | 98 | aaTx.customData = { 99 | ...aaTx.customData, 100 | customSignature: signature, 101 | } 102 | 103 | console.log( 104 | `The minimal account nonce before the first tx is ${await provider.getTransactionCount( 105 | ZK_MINIMAL_ADDRESS, 106 | )}`, 107 | ) 108 | 109 | const sentTx = await provider.broadcastTransaction( 110 | types.Transaction.from(aaTx).serialized, 111 | ) 112 | 113 | console.log(`Transaction sent from minimal account with hash ${sentTx.hash}`) 114 | await sentTx.wait() 115 | 116 | // Checking that the nonce for the account has increased 117 | console.log( 118 | `The account's nonce after the first tx is ${await provider.getTransactionCount( 119 | ZK_MINIMAL_ADDRESS, 120 | )}`, 121 | ) 122 | } 123 | 124 | main() 125 | .then(() => process.exit(0)) 126 | .catch((error) => { 127 | console.error(error) 128 | process.exit(1) 129 | }) 130 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@types/fs-extra": "^11.0.4", 4 | "dotenv": "^16.4.5", 5 | "ethers": "6", 6 | "fs-extra": "^11.2.0", 7 | "typescript": "^5.4.5", 8 | "zksync-ethers": "^6.8.0" 9 | }, 10 | "scripts": { 11 | "encryptKey": "ts-node javascript-scripts/EncryptKey.ts", 12 | "deploy": "ts-node javascript-scripts/DeployZkMinimal.ts", 13 | "sendTx": "ts-node javascript-scripts/SendAATx.ts", 14 | "compile": "forge build --zksync" 15 | } 16 | } -------------------------------------------------------------------------------- /script/DeployMinimal.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.24; 3 | 4 | import {Script} from "forge-std/Script.sol"; 5 | import {MinimalAccount} from "src/ethereum/MinimalAccount.sol"; 6 | import {HelperConfig} from "script/HelperConfig.s.sol"; 7 | 8 | contract DeployMinimal is Script { 9 | function run() public { 10 | deployMinimalAccount(); 11 | } 12 | 13 | function deployMinimalAccount() public returns (HelperConfig, MinimalAccount) { 14 | HelperConfig helperConfig = new HelperConfig(); 15 | HelperConfig.NetworkConfig memory config = helperConfig.getConfig(); 16 | 17 | vm.startBroadcast(config.account); 18 | MinimalAccount minimalAccount = new MinimalAccount(config.entryPoint); 19 | minimalAccount.transferOwnership(config.account); 20 | vm.stopBroadcast(); 21 | return (helperConfig, minimalAccount); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /script/HelperConfig.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.24; 3 | 4 | import {Script, console2} from "forge-std/Script.sol"; 5 | import {EntryPoint} from "lib/account-abstraction/contracts/core/EntryPoint.sol"; 6 | import {ERC20Mock} from "@openzeppelin/contracts/mocks/token/ERC20Mock.sol"; 7 | 8 | contract HelperConfig is Script { 9 | /*////////////////////////////////////////////////////////////// 10 | ERRORS 11 | //////////////////////////////////////////////////////////////*/ 12 | error HelperConfig__InvalidChainId(); 13 | 14 | /*////////////////////////////////////////////////////////////// 15 | TYPES 16 | //////////////////////////////////////////////////////////////*/ 17 | struct NetworkConfig { 18 | address entryPoint; 19 | address usdc; 20 | address account; 21 | } 22 | 23 | /*////////////////////////////////////////////////////////////// 24 | STATE VARIABLES 25 | //////////////////////////////////////////////////////////////*/ 26 | uint256 constant ETH_MAINNET_CHAIN_ID = 1; 27 | uint256 constant ETH_SEPOLIA_CHAIN_ID = 11155111; 28 | uint256 constant ZKSYNC_SEPOLIA_CHAIN_ID = 300; 29 | uint256 constant LOCAL_CHAIN_ID = 31337; 30 | // Update the BURNER_WALLET to your burner wallet! 31 | address constant BURNER_WALLET = 0x643315C9Be056cDEA171F4e7b2222a4ddaB9F88D; 32 | uint256 constant ARBITRUM_MAINNET_CHAIN_ID = 42_161; 33 | uint256 constant ZKSYNC_MAINNET_CHAIN_ID = 324; 34 | // address constant FOUNDRY_DEFAULT_WALLET = 0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38; 35 | address constant ANVIL_DEFAULT_ACCOUNT = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266; 36 | 37 | NetworkConfig public localNetworkConfig; 38 | mapping(uint256 chainId => NetworkConfig) public networkConfigs; 39 | 40 | /*////////////////////////////////////////////////////////////// 41 | FUNCTIONS 42 | //////////////////////////////////////////////////////////////*/ 43 | constructor() { 44 | networkConfigs[ETH_SEPOLIA_CHAIN_ID] = getEthSepoliaConfig(); 45 | networkConfigs[ETH_MAINNET_CHAIN_ID] = getEthMainnetConfig(); 46 | networkConfigs[ZKSYNC_MAINNET_CHAIN_ID] = getZkSyncConfig(); 47 | networkConfigs[ARBITRUM_MAINNET_CHAIN_ID] = getArbMainnetConfig(); 48 | } 49 | 50 | function getConfig() public returns (NetworkConfig memory) { 51 | return getConfigByChainId(block.chainid); 52 | } 53 | 54 | function getConfigByChainId(uint256 chainId) public returns (NetworkConfig memory) { 55 | if (chainId == LOCAL_CHAIN_ID) { 56 | return getOrCreateAnvilEthConfig(); 57 | } else if (networkConfigs[chainId].account != address(0)) { 58 | return networkConfigs[chainId]; 59 | } else { 60 | revert HelperConfig__InvalidChainId(); 61 | } 62 | } 63 | 64 | /*////////////////////////////////////////////////////////////// 65 | CONFIGS 66 | //////////////////////////////////////////////////////////////*/ 67 | function getEthMainnetConfig() public pure returns (NetworkConfig memory) { 68 | // This is v7 69 | return NetworkConfig({ 70 | entryPoint: 0x0000000071727De22E5E9d8BAf0edAc6f37da032, 71 | usdc: 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48, 72 | account: BURNER_WALLET 73 | }); 74 | // https://blockscan.com/address/0x0000000071727De22E5E9d8BAf0edAc6f37da032 75 | } 76 | 77 | function getEthSepoliaConfig() public pure returns (NetworkConfig memory) { 78 | return NetworkConfig({ 79 | entryPoint: 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789, 80 | usdc: 0x53844F9577C2334e541Aec7Df7174ECe5dF1fCf0, // Update with your own mock token 81 | account: BURNER_WALLET 82 | }); 83 | } 84 | 85 | function getArbMainnetConfig() public pure returns (NetworkConfig memory) { 86 | return NetworkConfig({ 87 | entryPoint: 0x0000000071727De22E5E9d8BAf0edAc6f37da032, 88 | usdc: 0xaf88d065e77c8cC2239327C5EDb3A432268e5831, 89 | account: BURNER_WALLET 90 | }); 91 | } 92 | 93 | function getZkSyncSepoliaConfig() public pure returns (NetworkConfig memory) { 94 | return NetworkConfig({ 95 | entryPoint: address(0), // There is no entrypoint in zkSync! 96 | usdc: 0x5A7d6b2F92C77FAD6CCaBd7EE0624E64907Eaf3E, // not the real USDC on zksync sepolia 97 | account: BURNER_WALLET 98 | }); 99 | } 100 | 101 | function getZkSyncConfig() public pure returns (NetworkConfig memory) { 102 | return NetworkConfig({ 103 | entryPoint: address(0), // supports native AA, so no entry point needed 104 | usdc: 0x1d17CBcF0D6D143135aE902365D2E5e2A16538D4, 105 | account: BURNER_WALLET 106 | }); 107 | } 108 | 109 | function getOrCreateAnvilEthConfig() public returns (NetworkConfig memory) { 110 | if (localNetworkConfig.account != address(0)) { 111 | return localNetworkConfig; 112 | } 113 | 114 | // deploy mocks 115 | console2.log("Deploying mocks..."); 116 | vm.startBroadcast(ANVIL_DEFAULT_ACCOUNT); 117 | EntryPoint entryPoint = new EntryPoint(); 118 | ERC20Mock erc20Mock = new ERC20Mock(); 119 | vm.stopBroadcast(); 120 | console2.log("Mocks deployed!"); 121 | 122 | localNetworkConfig = 123 | NetworkConfig({entryPoint: address(entryPoint), usdc: address(erc20Mock), account: ANVIL_DEFAULT_ACCOUNT}); 124 | return localNetworkConfig; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /script/SendPackedUserOp.s.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.24; 3 | 4 | import {Script, console2} from "forge-std/Script.sol"; 5 | import {PackedUserOperation} from "lib/account-abstraction/contracts/interfaces/PackedUserOperation.sol"; 6 | import {HelperConfig} from "script/HelperConfig.s.sol"; 7 | import {IEntryPoint} from "lib/account-abstraction/contracts/interfaces/IEntryPoint.sol"; 8 | import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; 9 | import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 10 | import {MinimalAccount} from "src/ethereum/MinimalAccount.sol"; 11 | import {DevOpsTools} from "lib/foundry-devops/src/DevOpsTools.sol"; 12 | 13 | contract SendPackedUserOp is Script { 14 | using MessageHashUtils for bytes32; 15 | 16 | // Make sure you trust this user - don't run this on Mainnet! 17 | address constant RANDOM_APPROVER = 0x9EA9b0cc1919def1A3CfAEF4F7A66eE3c36F86fC; 18 | 19 | function run() public { 20 | // Setup 21 | HelperConfig helperConfig = new HelperConfig(); 22 | address dest = helperConfig.getConfig().usdc; // arbitrum mainnet USDC address 23 | uint256 value = 0; 24 | address minimalAccountAddress = DevOpsTools.get_most_recent_deployment("MinimalAccount", block.chainid); 25 | 26 | bytes memory functionData = abi.encodeWithSelector(IERC20.approve.selector, RANDOM_APPROVER, 1e18); 27 | bytes memory executeCalldata = 28 | abi.encodeWithSelector(MinimalAccount.execute.selector, dest, value, functionData); 29 | PackedUserOperation memory userOp = 30 | generateSignedUserOperation(executeCalldata, helperConfig.getConfig(), minimalAccountAddress); 31 | PackedUserOperation[] memory ops = new PackedUserOperation[](1); 32 | ops[0] = userOp; 33 | 34 | // Send transaction 35 | vm.startBroadcast(); 36 | IEntryPoint(helperConfig.getConfig().entryPoint).handleOps(ops, payable(helperConfig.getConfig().account)); 37 | vm.stopBroadcast(); 38 | } 39 | 40 | function generateSignedUserOperation( 41 | bytes memory callData, 42 | HelperConfig.NetworkConfig memory config, 43 | address minimalAccount 44 | ) public view returns (PackedUserOperation memory) { 45 | // 1. Generate the unsigned data 46 | uint256 nonce = vm.getNonce(minimalAccount) - 1; 47 | PackedUserOperation memory userOp = _generateUnsignedUserOperation(callData, minimalAccount, nonce); 48 | 49 | // 2. Get the userOp Hash 50 | bytes32 userOpHash = IEntryPoint(config.entryPoint).getUserOpHash(userOp); 51 | bytes32 digest = userOpHash.toEthSignedMessageHash(); 52 | 53 | // 3. Sign it 54 | uint8 v; 55 | bytes32 r; 56 | bytes32 s; 57 | uint256 ANVIL_DEFAULT_KEY = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80; 58 | if (block.chainid == 31337) { 59 | (v, r, s) = vm.sign(ANVIL_DEFAULT_KEY, digest); 60 | } else { 61 | (v, r, s) = vm.sign(config.account, digest); 62 | } 63 | userOp.signature = abi.encodePacked(r, s, v); // Note the order 64 | return userOp; 65 | } 66 | 67 | function _generateUnsignedUserOperation(bytes memory callData, address sender, uint256 nonce) 68 | internal 69 | pure 70 | returns (PackedUserOperation memory) 71 | { 72 | uint128 verificationGasLimit = 16777216; 73 | uint128 callGasLimit = verificationGasLimit; 74 | uint128 maxPriorityFeePerGas = 256; 75 | uint128 maxFeePerGas = maxPriorityFeePerGas; 76 | return PackedUserOperation({ 77 | sender: sender, 78 | nonce: nonce, 79 | initCode: hex"", 80 | callData: callData, 81 | accountGasLimits: bytes32(uint256(verificationGasLimit) << 128 | callGasLimit), 82 | preVerificationGas: verificationGasLimit, 83 | gasFees: bytes32(uint256(maxPriorityFeePerGas) << 128 | maxFeePerGas), 84 | paymasterAndData: hex"", 85 | signature: hex"" 86 | }); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/ethereum/MinimalAccount.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.24; 3 | 4 | import {IAccount} from "lib/account-abstraction/contracts/interfaces/IAccount.sol"; 5 | import {PackedUserOperation} from "lib/account-abstraction/contracts/interfaces/PackedUserOperation.sol"; 6 | import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; 7 | import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; 8 | import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 9 | import {SIG_VALIDATION_FAILED, SIG_VALIDATION_SUCCESS} from "lib/account-abstraction/contracts/core/Helpers.sol"; 10 | import {IEntryPoint} from "lib/account-abstraction/contracts/interfaces/IEntryPoint.sol"; 11 | 12 | contract MinimalAccount is IAccount, Ownable { 13 | /*////////////////////////////////////////////////////////////// 14 | ERRORS 15 | //////////////////////////////////////////////////////////////*/ 16 | error MinimalAccount__NotFromEntryPoint(); 17 | error MinimalAccount__NotFromEntryPointOrOwner(); 18 | error MinimalAccount__CallFailed(bytes); 19 | 20 | /*////////////////////////////////////////////////////////////// 21 | STATE VARIABLES 22 | //////////////////////////////////////////////////////////////*/ 23 | IEntryPoint private immutable i_entryPoint; 24 | 25 | /*////////////////////////////////////////////////////////////// 26 | MODIFIERS 27 | //////////////////////////////////////////////////////////////*/ 28 | modifier requireFromEntryPoint() { 29 | if (msg.sender != address(i_entryPoint)) { 30 | revert MinimalAccount__NotFromEntryPoint(); 31 | } 32 | _; 33 | } 34 | 35 | modifier requireFromEntryPointOrOwner() { 36 | if (msg.sender != address(i_entryPoint) && msg.sender != owner()) { 37 | revert MinimalAccount__NotFromEntryPointOrOwner(); 38 | } 39 | _; 40 | } 41 | 42 | /*////////////////////////////////////////////////////////////// 43 | FUNCTIONS 44 | //////////////////////////////////////////////////////////////*/ 45 | constructor(address entryPoint) Ownable(msg.sender) { 46 | i_entryPoint = IEntryPoint(entryPoint); 47 | } 48 | 49 | receive() external payable {} 50 | 51 | /*////////////////////////////////////////////////////////////// 52 | EXTERNAL FUNCTIONS 53 | //////////////////////////////////////////////////////////////*/ 54 | function execute(address dest, uint256 value, bytes calldata functionData) external requireFromEntryPointOrOwner { 55 | (bool success, bytes memory result) = dest.call{value: value}(functionData); 56 | if (!success) { 57 | revert MinimalAccount__CallFailed(result); 58 | } 59 | } 60 | 61 | // A signature is valid, if it's the MinimalAccount owner 62 | function validateUserOp(PackedUserOperation calldata userOp, bytes32 userOpHash, uint256 missingAccountFunds) 63 | external 64 | requireFromEntryPoint 65 | returns (uint256 validationData) 66 | { 67 | validationData = _validateSignature(userOp, userOpHash); 68 | // _validateNonce() 69 | _payPrefund(missingAccountFunds); 70 | } 71 | 72 | /*////////////////////////////////////////////////////////////// 73 | INTERNAL FUNCTIONS 74 | //////////////////////////////////////////////////////////////*/ 75 | // EIP-191 version of the signed hash 76 | function _validateSignature(PackedUserOperation calldata userOp, bytes32 userOpHash) 77 | internal 78 | view 79 | returns (uint256 validationData) 80 | { 81 | bytes32 ethSignedMessageHash = MessageHashUtils.toEthSignedMessageHash(userOpHash); 82 | address signer = ECDSA.recover(ethSignedMessageHash, userOp.signature); 83 | if (signer != owner()) { 84 | return SIG_VALIDATION_FAILED; 85 | } 86 | return SIG_VALIDATION_SUCCESS; 87 | } 88 | 89 | function _payPrefund(uint256 missingAccountFunds) internal { 90 | if (missingAccountFunds != 0) { 91 | (bool success,) = payable(msg.sender).call{value: missingAccountFunds, gas: type(uint256).max}(""); 92 | (success); 93 | } 94 | } 95 | 96 | /*////////////////////////////////////////////////////////////// 97 | GETTERS 98 | //////////////////////////////////////////////////////////////*/ 99 | function getEntryPoint() external view returns (address) { 100 | return address(i_entryPoint); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/zksync/ZkMinimalAccount.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.24; 3 | 4 | // zkSync Era Imports 5 | import { 6 | IAccount, 7 | ACCOUNT_VALIDATION_SUCCESS_MAGIC 8 | } from "lib/foundry-era-contracts/src/system-contracts/contracts/interfaces/IAccount.sol"; 9 | import { 10 | Transaction, 11 | MemoryTransactionHelper 12 | } from "lib/foundry-era-contracts/src/system-contracts/contracts/libraries/MemoryTransactionHelper.sol"; 13 | import {SystemContractsCaller} from 14 | "lib/foundry-era-contracts/src/system-contracts/contracts/libraries/SystemContractsCaller.sol"; 15 | import { 16 | NONCE_HOLDER_SYSTEM_CONTRACT, 17 | BOOTLOADER_FORMAL_ADDRESS, 18 | DEPLOYER_SYSTEM_CONTRACT 19 | } from "lib/foundry-era-contracts/src/system-contracts/contracts/Constants.sol"; 20 | import {INonceHolder} from "lib/foundry-era-contracts/src/system-contracts/contracts/interfaces/INonceHolder.sol"; 21 | import {Utils} from "lib/foundry-era-contracts/src/system-contracts/contracts/libraries/Utils.sol"; 22 | 23 | // OZ Imports 24 | import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; 25 | import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 26 | import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; 27 | 28 | /** 29 | * Lifecycle of a type 113 (0x71) transaction 30 | * msg.sender is the bootloader system contract 31 | * 32 | * Phase 1 Validation 33 | * 1. The user sends the transaction to the "zkSync API client" (sort of a "light node") 34 | * 2. The zkSync API client checks to see the the nonce is unique by querying the NonceHolder system contract 35 | * 3. The zkSync API client calls validateTransaction, which MUST update the nonce 36 | * 4. The zkSync API client checks the nonce is updated 37 | * 5. The zkSync API client calls payForTransaction, or prepareForPaymaster & validateAndPayForPaymasterTransaction 38 | * 6. The zkSync API client verifies that the bootloader gets paid 39 | * 40 | * Phase 2 Execution 41 | * 7. The zkSync API client passes the validated transaction to the main node / sequencer (as of today, they are the same) 42 | * 8. The main node calls executeTransaction 43 | * 9. If a paymaster was used, the postTransaction is called 44 | */ 45 | contract ZkMinimalAccount is IAccount, Ownable { 46 | using MemoryTransactionHelper for Transaction; 47 | 48 | error ZkMinimalAccount__NotEnoughBalance(); 49 | error ZkMinimalAccount__NotFromBootLoader(); 50 | error ZkMinimalAccount__ExecutionFailed(); 51 | error ZkMinimalAccount__NotFromBootLoaderOrOwner(); 52 | error ZkMinimalAccount__FailedToPay(); 53 | error ZkMinimalAccount__InvalidSignature(); 54 | 55 | /*////////////////////////////////////////////////////////////// 56 | MODIFIERS 57 | //////////////////////////////////////////////////////////////*/ 58 | modifier requireFromBootLoader() { 59 | if (msg.sender != BOOTLOADER_FORMAL_ADDRESS) { 60 | revert ZkMinimalAccount__NotFromBootLoader(); 61 | } 62 | _; 63 | } 64 | 65 | modifier requireFromBootLoaderOrOwner() { 66 | if (msg.sender != BOOTLOADER_FORMAL_ADDRESS && msg.sender != owner()) { 67 | revert ZkMinimalAccount__NotFromBootLoaderOrOwner(); 68 | } 69 | _; 70 | } 71 | 72 | constructor() Ownable(msg.sender) {} 73 | 74 | receive() external payable {} 75 | 76 | /*////////////////////////////////////////////////////////////// 77 | EXTERNAL FUNCTIONS 78 | //////////////////////////////////////////////////////////////*/ 79 | /** 80 | * @notice must increase the nonce 81 | * @notice must validate the transaction (check the owner signed the transaction) 82 | * @notice also check to see if we have enough money in our account 83 | */ 84 | function validateTransaction(bytes32, /*_txHash*/ bytes32, /*_suggestedSignedHash*/ Transaction memory _transaction) 85 | external 86 | payable 87 | requireFromBootLoader 88 | returns (bytes4 magic) 89 | { 90 | return _validateTransaction(_transaction); 91 | } 92 | 93 | function executeTransaction(bytes32, /*_txHash*/ bytes32, /*_suggestedSignedHash*/ Transaction memory _transaction) 94 | external 95 | payable 96 | requireFromBootLoaderOrOwner 97 | { 98 | _executeTransaction(_transaction); 99 | } 100 | 101 | function executeTransactionFromOutside(Transaction memory _transaction) external payable { 102 | bytes4 magic = _validateTransaction(_transaction); 103 | if (magic != ACCOUNT_VALIDATION_SUCCESS_MAGIC) { 104 | revert ZkMinimalAccount__InvalidSignature(); 105 | } 106 | _executeTransaction(_transaction); 107 | } 108 | 109 | function payForTransaction(bytes32, /*_txHash*/ bytes32, /*_suggestedSignedHash*/ Transaction memory _transaction) 110 | external 111 | payable 112 | { 113 | bool success = _transaction.payToTheBootloader(); 114 | if (!success) { 115 | revert ZkMinimalAccount__FailedToPay(); 116 | } 117 | } 118 | 119 | function prepareForPaymaster(bytes32 _txHash, bytes32 _possibleSignedHash, Transaction memory _transaction) 120 | external 121 | payable 122 | {} 123 | 124 | /*////////////////////////////////////////////////////////////// 125 | INTERNAL FUNCTIONS 126 | //////////////////////////////////////////////////////////////*/ 127 | function _validateTransaction(Transaction memory _transaction) internal returns (bytes4 magic) { 128 | // Call nonceholder 129 | // increment nonce 130 | // call(x, y, z) -> system contract call 131 | SystemContractsCaller.systemCallWithPropagatedRevert( 132 | uint32(gasleft()), 133 | address(NONCE_HOLDER_SYSTEM_CONTRACT), 134 | 0, 135 | abi.encodeCall(INonceHolder.incrementMinNonceIfEquals, (_transaction.nonce)) 136 | ); 137 | 138 | // Check for fee to pay 139 | uint256 totalRequiredBalance = _transaction.totalRequiredBalance(); 140 | if (totalRequiredBalance > address(this).balance) { 141 | revert ZkMinimalAccount__NotEnoughBalance(); 142 | } 143 | 144 | // Check the signature 145 | bytes32 txHash = _transaction.encodeHash(); 146 | // bytes32 convertedHash = MessageHashUtils.toEthSignedMessageHash(txHash); 147 | address signer = ECDSA.recover(txHash, _transaction.signature); 148 | bool isValidSigner = signer == owner(); 149 | if (isValidSigner) { 150 | magic = ACCOUNT_VALIDATION_SUCCESS_MAGIC; 151 | } else { 152 | magic = bytes4(0); 153 | } 154 | return magic; 155 | } 156 | 157 | function _executeTransaction(Transaction memory _transaction) internal { 158 | address to = address(uint160(_transaction.to)); 159 | uint128 value = Utils.safeCastToU128(_transaction.value); 160 | bytes memory data = _transaction.data; 161 | 162 | if (to == address(DEPLOYER_SYSTEM_CONTRACT)) { 163 | uint32 gas = Utils.safeCastToU32(gasleft()); 164 | SystemContractsCaller.systemCallWithPropagatedRevert(gas, to, value, data); 165 | } else { 166 | bool success; 167 | assembly { 168 | success := call(gas(), to, value, add(data, 0x20), mload(data), 0, 0) 169 | } 170 | if (!success) { 171 | revert ZkMinimalAccount__ExecutionFailed(); 172 | } 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /test/ethereum/MinimalAccountTest.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.24; 3 | 4 | import {Test} from "forge-std/Test.sol"; 5 | import {MinimalAccount} from "src/ethereum/MinimalAccount.sol"; 6 | import {DeployMinimal} from "script/DeployMinimal.s.sol"; 7 | import {HelperConfig} from "script/HelperConfig.s.sol"; 8 | import {ERC20Mock} from "@openzeppelin/contracts/mocks/token/ERC20Mock.sol"; 9 | import {SendPackedUserOp, PackedUserOperation, IEntryPoint} from "script/SendPackedUserOp.s.sol"; 10 | import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; 11 | import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; 12 | import {ZkSyncChainChecker} from "lib/foundry-devops/src/ZkSyncChainChecker.sol"; 13 | 14 | contract MinimalAccountTest is Test, ZkSyncChainChecker { 15 | using MessageHashUtils for bytes32; 16 | 17 | HelperConfig helperConfig; 18 | MinimalAccount minimalAccount; 19 | ERC20Mock usdc; 20 | SendPackedUserOp sendPackedUserOp; 21 | 22 | address randomuser = makeAddr("randomUser"); 23 | 24 | uint256 constant AMOUNT = 1e18; 25 | 26 | function setUp() public skipZkSync { 27 | DeployMinimal deployMinimal = new DeployMinimal(); 28 | (helperConfig, minimalAccount) = deployMinimal.deployMinimalAccount(); 29 | usdc = new ERC20Mock(); 30 | sendPackedUserOp = new SendPackedUserOp(); 31 | } 32 | 33 | // USDC Mint 34 | // msg.sender -> MinimalAccount 35 | // approve some amount 36 | // USDC contract 37 | // come from the entrypoint 38 | function testOwnerCanExecuteCommands() public skipZkSync { 39 | // Arrange 40 | assertEq(usdc.balanceOf(address(minimalAccount)), 0); 41 | address dest = address(usdc); 42 | uint256 value = 0; 43 | bytes memory functionData = abi.encodeWithSelector(ERC20Mock.mint.selector, address(minimalAccount), AMOUNT); 44 | // Act 45 | vm.prank(minimalAccount.owner()); 46 | minimalAccount.execute(dest, value, functionData); 47 | 48 | // Assert 49 | assertEq(usdc.balanceOf(address(minimalAccount)), AMOUNT); 50 | } 51 | 52 | function testNonOwnerCannotExecuteCommands() public skipZkSync { 53 | // Arrange 54 | assertEq(usdc.balanceOf(address(minimalAccount)), 0); 55 | address dest = address(usdc); 56 | uint256 value = 0; 57 | bytes memory functionData = abi.encodeWithSelector(ERC20Mock.mint.selector, address(minimalAccount), AMOUNT); 58 | // Act 59 | vm.prank(randomuser); 60 | vm.expectRevert(MinimalAccount.MinimalAccount__NotFromEntryPointOrOwner.selector); 61 | minimalAccount.execute(dest, value, functionData); 62 | } 63 | 64 | function testRecoverSignedOp() public skipZkSync { 65 | // Arrange 66 | assertEq(usdc.balanceOf(address(minimalAccount)), 0); 67 | address dest = address(usdc); 68 | uint256 value = 0; 69 | bytes memory functionData = abi.encodeWithSelector(ERC20Mock.mint.selector, address(minimalAccount), AMOUNT); 70 | bytes memory executeCallData = 71 | abi.encodeWithSelector(MinimalAccount.execute.selector, dest, value, functionData); 72 | PackedUserOperation memory packedUserOp = sendPackedUserOp.generateSignedUserOperation( 73 | executeCallData, helperConfig.getConfig(), address(minimalAccount) 74 | ); 75 | bytes32 userOperationHash = IEntryPoint(helperConfig.getConfig().entryPoint).getUserOpHash(packedUserOp); 76 | 77 | // Act 78 | address actualSigner = ECDSA.recover(userOperationHash.toEthSignedMessageHash(), packedUserOp.signature); 79 | 80 | // Assert 81 | assertEq(actualSigner, minimalAccount.owner()); 82 | } 83 | 84 | // 1. Sign user ops 85 | // 2. Call validate userops 86 | // 3. Assert the return is correct 87 | function testValidationOfUserOps() public skipZkSync { 88 | // Arrange 89 | assertEq(usdc.balanceOf(address(minimalAccount)), 0); 90 | address dest = address(usdc); 91 | uint256 value = 0; 92 | bytes memory functionData = abi.encodeWithSelector(ERC20Mock.mint.selector, address(minimalAccount), AMOUNT); 93 | bytes memory executeCallData = 94 | abi.encodeWithSelector(MinimalAccount.execute.selector, dest, value, functionData); 95 | PackedUserOperation memory packedUserOp = sendPackedUserOp.generateSignedUserOperation( 96 | executeCallData, helperConfig.getConfig(), address(minimalAccount) 97 | ); 98 | bytes32 userOperationHash = IEntryPoint(helperConfig.getConfig().entryPoint).getUserOpHash(packedUserOp); 99 | uint256 missingAccountFunds = 1e18; 100 | 101 | // Act 102 | vm.prank(helperConfig.getConfig().entryPoint); 103 | uint256 validationData = minimalAccount.validateUserOp(packedUserOp, userOperationHash, missingAccountFunds); 104 | assertEq(validationData, 0); 105 | } 106 | 107 | function testEntryPointCanExecuteCommands() public skipZkSync { 108 | // Arrange 109 | assertEq(usdc.balanceOf(address(minimalAccount)), 0); 110 | address dest = address(usdc); 111 | uint256 value = 0; 112 | bytes memory functionData = abi.encodeWithSelector(ERC20Mock.mint.selector, address(minimalAccount), AMOUNT); 113 | bytes memory executeCallData = 114 | abi.encodeWithSelector(MinimalAccount.execute.selector, dest, value, functionData); 115 | PackedUserOperation memory packedUserOp = sendPackedUserOp.generateSignedUserOperation( 116 | executeCallData, helperConfig.getConfig(), address(minimalAccount) 117 | ); 118 | // bytes32 userOperationHash = IEntryPoint(helperConfig.getConfig().entryPoint).getUserOpHash(packedUserOp); 119 | 120 | vm.deal(address(minimalAccount), 1e18); 121 | 122 | PackedUserOperation[] memory ops = new PackedUserOperation[](1); 123 | ops[0] = packedUserOp; 124 | 125 | // Act 126 | vm.prank(randomuser); 127 | IEntryPoint(helperConfig.getConfig().entryPoint).handleOps(ops, payable(randomuser)); 128 | 129 | // Assert 130 | assertEq(usdc.balanceOf(address(minimalAccount)), AMOUNT); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /test/zksync/ZkMinimalAccountTest.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.24; 3 | 4 | import {Test, console2} from "forge-std/Test.sol"; 5 | import {ZkMinimalAccount} from "src/zksync/ZkMinimalAccount.sol"; 6 | import {ERC20Mock} from "@openzeppelin/contracts/mocks/token/ERC20Mock.sol"; 7 | 8 | // Era Imports 9 | import { 10 | Transaction, 11 | MemoryTransactionHelper 12 | } from "lib/foundry-era-contracts/src/system-contracts/contracts/libraries/MemoryTransactionHelper.sol"; 13 | import {BOOTLOADER_FORMAL_ADDRESS} from "lib/foundry-era-contracts/src/system-contracts/contracts/Constants.sol"; 14 | import {ACCOUNT_VALIDATION_SUCCESS_MAGIC} from 15 | "lib/foundry-era-contracts/src/system-contracts/contracts/interfaces/IAccount.sol"; 16 | 17 | // OZ Imports 18 | import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; 19 | 20 | // Foundry Devops 21 | import {ZkSyncChainChecker} from "lib/foundry-devops/src/ZkSyncChainChecker.sol"; 22 | 23 | contract ZkMinimalAccountTest is Test, ZkSyncChainChecker { 24 | using MessageHashUtils for bytes32; 25 | 26 | ZkMinimalAccount minimalAccount; 27 | ERC20Mock usdc; 28 | bytes4 constant EIP1271_SUCCESS_RETURN_VALUE = 0x1626ba7e; 29 | 30 | uint256 constant AMOUNT = 1e18; 31 | bytes32 constant EMPTY_BYTES32 = bytes32(0); 32 | address constant ANVIL_DEFAULT_ACCOUNT = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266; 33 | 34 | function setUp() public { 35 | minimalAccount = new ZkMinimalAccount(); 36 | minimalAccount.transferOwnership(ANVIL_DEFAULT_ACCOUNT); 37 | usdc = new ERC20Mock(); 38 | vm.deal(address(minimalAccount), AMOUNT); 39 | } 40 | 41 | function testZkOwnerCanExecuteCommands() public { 42 | // Arrange 43 | address dest = address(usdc); 44 | uint256 value = 0; 45 | bytes memory functionData = abi.encodeWithSelector(ERC20Mock.mint.selector, address(minimalAccount), AMOUNT); 46 | 47 | Transaction memory transaction = 48 | _createUnsignedTransaction(minimalAccount.owner(), 113, dest, value, functionData); 49 | 50 | // Act 51 | vm.prank(minimalAccount.owner()); 52 | minimalAccount.executeTransaction(EMPTY_BYTES32, EMPTY_BYTES32, transaction); 53 | 54 | // Assert 55 | assertEq(usdc.balanceOf(address(minimalAccount)), AMOUNT); 56 | } 57 | 58 | // You'll also need --system-mode=true to run this test 59 | function testZkValidateTransaction() public onlyZkSync { 60 | // Arrange 61 | address dest = address(usdc); 62 | uint256 value = 0; 63 | bytes memory functionData = abi.encodeWithSelector(ERC20Mock.mint.selector, address(minimalAccount), AMOUNT); 64 | Transaction memory transaction = 65 | _createUnsignedTransaction(minimalAccount.owner(), 113, dest, value, functionData); 66 | transaction = _signTransaction(transaction); 67 | 68 | // Act 69 | vm.prank(BOOTLOADER_FORMAL_ADDRESS); 70 | bytes4 magic = minimalAccount.validateTransaction(EMPTY_BYTES32, EMPTY_BYTES32, transaction); 71 | 72 | // Assert 73 | assertEq(magic, ACCOUNT_VALIDATION_SUCCESS_MAGIC); 74 | } 75 | 76 | /*////////////////////////////////////////////////////////////// 77 | HELPERS 78 | //////////////////////////////////////////////////////////////*/ 79 | function _signTransaction(Transaction memory transaction) internal view returns (Transaction memory) { 80 | bytes32 unsignedTransactionHash = MemoryTransactionHelper.encodeHash(transaction); 81 | // bytes32 digest = unsignedTransactionHash.toEthSignedMessageHash(); 82 | uint8 v; 83 | bytes32 r; 84 | bytes32 s; 85 | uint256 ANVIL_DEFAULT_KEY = 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80; 86 | (v, r, s) = vm.sign(ANVIL_DEFAULT_KEY, unsignedTransactionHash); 87 | Transaction memory signedTransaction = transaction; 88 | signedTransaction.signature = abi.encodePacked(r, s, v); 89 | return signedTransaction; 90 | } 91 | 92 | function _createUnsignedTransaction( 93 | address from, 94 | uint8 transactionType, 95 | address to, 96 | uint256 value, 97 | bytes memory data 98 | ) internal view returns (Transaction memory) { 99 | uint256 nonce = vm.getNonce(address(minimalAccount)); 100 | bytes32[] memory factoryDeps = new bytes32[](0); 101 | return Transaction({ 102 | txType: transactionType, // type 113 (0x71). 103 | from: uint256(uint160(from)), 104 | to: uint256(uint160(to)), 105 | gasLimit: 16777216, 106 | gasPerPubdataByteLimit: 16777216, 107 | maxFeePerGas: 16777216, 108 | maxPriorityFeePerGas: 16777216, 109 | paymaster: 0, 110 | nonce: nonce, 111 | value: value, 112 | reserved: [uint256(0), uint256(0), uint256(0), uint256(0)], 113 | data: data, 114 | signature: hex"", 115 | factoryDeps: factoryDeps, 116 | paymasterInput: hex"", 117 | reservedDynamic: hex"" 118 | }); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@adraffy/ens-normalize@1.10.1": 6 | version "1.10.1" 7 | resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz#63430d04bd8c5e74f8d7d049338f1cd9d4f02069" 8 | integrity sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw== 9 | 10 | "@noble/curves@1.2.0": 11 | version "1.2.0" 12 | resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.2.0.tgz#92d7e12e4e49b23105a2555c6984d41733d65c35" 13 | integrity sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw== 14 | dependencies: 15 | "@noble/hashes" "1.3.2" 16 | 17 | "@noble/hashes@1.3.2": 18 | version "1.3.2" 19 | resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" 20 | integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== 21 | 22 | "@types/fs-extra@^11.0.4": 23 | version "11.0.4" 24 | resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-11.0.4.tgz#e16a863bb8843fba8c5004362b5a73e17becca45" 25 | integrity sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ== 26 | dependencies: 27 | "@types/jsonfile" "*" 28 | "@types/node" "*" 29 | 30 | "@types/jsonfile@*": 31 | version "6.1.4" 32 | resolved "https://registry.yarnpkg.com/@types/jsonfile/-/jsonfile-6.1.4.tgz#614afec1a1164e7d670b4a7ad64df3e7beb7b702" 33 | integrity sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ== 34 | dependencies: 35 | "@types/node" "*" 36 | 37 | "@types/node@*": 38 | version "20.14.8" 39 | resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.8.tgz#45c26a2a5de26c3534a9504530ddb3b27ce031ac" 40 | integrity sha512-DO+2/jZinXfROG7j7WKFn/3C6nFwxy2lLpgLjEXJz+0XKphZlTLJ14mo8Vfg8X5BWN6XjyESXq+LcYdT7tR3bA== 41 | dependencies: 42 | undici-types "~5.26.4" 43 | 44 | "@types/node@18.15.13": 45 | version "18.15.13" 46 | resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.13.tgz#f64277c341150c979e42b00e4ac289290c9df469" 47 | integrity sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q== 48 | 49 | aes-js@4.0.0-beta.5: 50 | version "4.0.0-beta.5" 51 | resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-4.0.0-beta.5.tgz#8d2452c52adedebc3a3e28465d858c11ca315873" 52 | integrity sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q== 53 | 54 | dotenv@^16.4.5: 55 | version "16.4.5" 56 | resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" 57 | integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== 58 | 59 | ethers@6: 60 | version "6.13.1" 61 | resolved "https://registry.yarnpkg.com/ethers/-/ethers-6.13.1.tgz#2b9f9c7455cde9d38b30fe6589972eb083652961" 62 | integrity sha512-hdJ2HOxg/xx97Lm9HdCWk949BfYqYWpyw4//78SiwOLgASyfrNszfMUNB2joKjvGUdwhHfaiMMFFwacVVoLR9A== 63 | dependencies: 64 | "@adraffy/ens-normalize" "1.10.1" 65 | "@noble/curves" "1.2.0" 66 | "@noble/hashes" "1.3.2" 67 | "@types/node" "18.15.13" 68 | aes-js "4.0.0-beta.5" 69 | tslib "2.4.0" 70 | ws "8.17.1" 71 | 72 | fs-extra@^11.2.0: 73 | version "11.2.0" 74 | resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b" 75 | integrity sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw== 76 | dependencies: 77 | graceful-fs "^4.2.0" 78 | jsonfile "^6.0.1" 79 | universalify "^2.0.0" 80 | 81 | graceful-fs@^4.1.6, graceful-fs@^4.2.0: 82 | version "4.2.11" 83 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" 84 | integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== 85 | 86 | jsonfile@^6.0.1: 87 | version "6.1.0" 88 | resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" 89 | integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== 90 | dependencies: 91 | universalify "^2.0.0" 92 | optionalDependencies: 93 | graceful-fs "^4.1.6" 94 | 95 | tslib@2.4.0: 96 | version "2.4.0" 97 | resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" 98 | integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== 99 | 100 | typescript@^5.4.5: 101 | version "5.5.2" 102 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.2.tgz#c26f023cb0054e657ce04f72583ea2d85f8d0507" 103 | integrity sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew== 104 | 105 | undici-types@~5.26.4: 106 | version "5.26.5" 107 | resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" 108 | integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== 109 | 110 | universalify@^2.0.0: 111 | version "2.0.1" 112 | resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" 113 | integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== 114 | 115 | ws@8.17.1: 116 | version "8.17.1" 117 | resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" 118 | integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== 119 | 120 | zksync-ethers@^6.8.0: 121 | version "6.9.0" 122 | resolved "https://registry.yarnpkg.com/zksync-ethers/-/zksync-ethers-6.9.0.tgz#efaff1d59e2cff837eeda84c4ba59fdca4972a91" 123 | integrity sha512-2CppwvLHtz689L7E9EhevbFtsqVukKC/lVicwdeUS2yqV46ET4iBR11rYdEfGW2oEo1h6yJuuwIBDFm2SybkIA== 124 | --------------------------------------------------------------------------------