├── circuits
├── zkecdsa
│ ├── Prover.toml
│ ├── Nargo.toml
│ ├── src
│ │ ├── ecrecover.nr
│ │ ├── array.nr
│ │ └── main.nr
│ └── Verifier.toml
└── hash
│ ├── Nargo.toml
│ └── src
│ └── main.nr
├── delete.sh
├── prove
├── prove_set.sh
├── prove_app_r.sh
├── prove_exec_i.sh
└── prove_prop_i.sh
├── remappings.txt
├── .gitignore
├── test
├── mocks
│ └── MockSetter.sol
├── utils
│ ├── userOp
│ │ ├── getUserOpHash.sol
│ │ └── Fixtures.sol
│ ├── BytesLib.sol
│ ├── NoirHelper.sol
│ └── pubkey
│ │ └── address.sol
└── Account.t.sol
├── src
├── modules
│ ├── ModuleBase.sol
│ ├── Recovery.sol
│ └── Inheritance.sol
└── erc4337
│ ├── AccountFactory.sol
│ ├── Account.sol
│ └── external
│ └── EntryPoint.sol
├── .gitmodules
├── .vscode
└── settings.json
├── foundry.toml
├── script
└── Deploy4337.s.sol
└── README.md
/circuits/zkecdsa/Prover.toml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/delete.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | cd circuits/zkecdsa/
3 | > Prover.toml
--------------------------------------------------------------------------------
/prove/prove_set.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | cd circuits/zkecdsa/src
3 | nargo prove set
--------------------------------------------------------------------------------
/prove/prove_app_r.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | cd circuits/zkecdsa/src
3 | nargo prove app_r
--------------------------------------------------------------------------------
/prove/prove_exec_i.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | cd circuits/zkecdsa/src
3 | nargo prove exec_i
--------------------------------------------------------------------------------
/prove/prove_prop_i.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | cd circuits/zkecdsa/src
3 | nargo prove prop_i
--------------------------------------------------------------------------------
/circuits/hash/Nargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | authors = [""]
3 | compiler_version = "0.6.0"
4 |
5 | [dependencies]
--------------------------------------------------------------------------------
/circuits/zkecdsa/Nargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | authors = [""]
3 | compiler_version = "0.6.0"
4 |
5 | [dependencies]
--------------------------------------------------------------------------------
/remappings.txt:
--------------------------------------------------------------------------------
1 | ds-test/=lib/forge-std/lib/ds-test/src/
2 | forge-std/=lib/forge-std/src/
3 | account-abstration/=lib/account-abstration/contracts/
4 | @openzeppelin/=lib/openzeppelin-contracts/
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | out/
2 | zkout/
3 | broadcast/
4 | cache/
5 | artifacts/
6 | circuits/zkecdsa/target/
7 | circuits/zkecdsa/contract/
8 | circuits/zkecdsa/proofs/
9 | yarn-error.log*
10 | .env
11 | trush/
12 | memo.md
13 |
--------------------------------------------------------------------------------
/test/mocks/MockSetter.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: GPL-3.0-only
2 | pragma solidity ^0.8.12;
3 |
4 | contract MockSetter {
5 | uint256 public value;
6 |
7 | function setValue(uint256 _value) public {
8 | value = _value;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/modules/ModuleBase.sol:
--------------------------------------------------------------------------------
1 | pragma solidity ^0.8.12;
2 |
3 | contract ModuleBase {
4 | modifier onlySelf() {
5 | require(msg.sender == address(this), "ONLY_SELF");
6 | _;
7 | }
8 |
9 | function changeOwner(bytes32 _owner) internal virtual {}
10 |
11 | function _owmer() internal view virtual returns (bytes32) {}
12 | }
13 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "lib/forge-std"]
2 | path = lib/forge-std
3 | url = https://github.com/foundry-rs/forge-std
4 | branch = v1.5.6
5 | [submodule "lib/openzeppelin-contracts"]
6 | path = lib/openzeppelin-contracts
7 | url = https://github.com/OpenZeppelin/openzeppelin-contracts
8 | branch = v4.8.3
9 | [submodule "lib/account-abstraction"]
10 | path = lib/account-abstraction
11 | url = https://github.com/eth-infinitism/account-abstraction
12 | branch = v0.6.0
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "solidity.packageDefaultDependenciesContractsDirectory": "src",
3 | "solidity.packageDefaultDependenciesDirectory": "lib",
4 | // "solidity.remappings": [
5 | // "ds-test/=lib/forge-std/lib/ds-test/src/",
6 | // "forge-std/=lib/forge-std/src/",
7 | // "account-abstration/=lib/account-abstration/contracts/",
8 | // "openzeppelin-contracts/=lib/openzeppelin-contracts/",
9 | // "era-system-contracts/=lib/era-system-contracts/"
10 | // ]
11 | }
--------------------------------------------------------------------------------
/foundry.toml:
--------------------------------------------------------------------------------
1 | # Foundry Configuration File
2 | # Default definitions: https://github.com/gakonst/foundry/blob/b7917fa8491aedda4dd6db53fbb206ea233cd531/config/src/lib.rs#L782
3 | # See more config options at: https://github.com/gakonst/foundry/tree/master/config
4 |
5 | # The Default Profile
6 | [profile.default]
7 | fs_permissions = [
8 | { access = "read-write", path = "./"}
9 | ]
10 |
11 | ffi = true
12 | solc_version = '0.8.12'
13 | auto_detect_solc = false
14 | optimizer_runs = 5_000
15 | optimizer = true
16 | src = "src"
17 | libs = ["lib"]
18 |
19 | [rpc_endpoints]
20 | ethereum="https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_API_KEY}"
21 | localhost="http://localhost:8545"
22 | goerli="https://goerli.infura.io/v3/${ALCHEMY_API_KEY_GOERLI}"
23 |
24 |
--------------------------------------------------------------------------------
/test/utils/userOp/getUserOpHash.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: GPL-3.0-only
2 | pragma solidity ^0.8.12;
3 |
4 | import {UserOperation} from "account-abstraction/interfaces/UserOperation.sol";
5 |
6 | /// @notice Get the userOperation hash over a user operation, entryPoint and chainId
7 | function getUserOpHash(
8 | UserOperation memory userOp,
9 | address entryPoint,
10 | uint256 chainId
11 | ) pure returns (bytes32) {
12 | bytes32 userOpHash = keccak256(
13 | abi.encode(
14 | userOp.sender,
15 | userOp.nonce,
16 | userOp.initCode,
17 | userOp.callData,
18 | userOp.callGasLimit,
19 | userOp.verificationGasLimit,
20 | userOp.preVerificationGas,
21 | userOp.maxFeePerGas,
22 | userOp.maxPriorityFeePerGas,
23 | userOp.paymasterAndData
24 | )
25 | );
26 |
27 | return keccak256(abi.encode(userOpHash, entryPoint, chainId));
28 | }
29 |
--------------------------------------------------------------------------------
/test/utils/userOp/Fixtures.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: GPL-3.0-only
2 | pragma solidity ^0.8.12;
3 |
4 | import {Vm} from "forge-std/Test.sol";
5 | import {UserOperation} from "account-abstraction/interfaces/UserOperation.sol";
6 | import {getUserOpHash} from "./getUserOpHash.sol";
7 |
8 | // Assumes chainId is 0x1, entryPoint is address(0x1). Hardcoded due to Solidity stack too deep errors, tricky to work around
9 | function getUserOperation(
10 | address sender,
11 | uint256 nonce,
12 | bytes memory callData,
13 | address entryPoint,
14 | uint8 chainId,
15 | Vm vm
16 | ) pure returns (UserOperation memory, bytes32) {
17 | UserOperation memory userOp = UserOperation({
18 | sender: sender,
19 | nonce: nonce,
20 | initCode: "",
21 | callData: callData,
22 | callGasLimit: 22017,
23 | verificationGasLimit: 958666,
24 | preVerificationGas: 115256,
25 | maxFeePerGas: 1000105660,
26 | maxPriorityFeePerGas: 1000000000,
27 | paymasterAndData: "",
28 | signature: ""
29 | });
30 | bytes32 userOpHash = getUserOpHash(userOp, entryPoint, chainId);
31 | return (userOp, userOpHash);
32 | }
33 |
--------------------------------------------------------------------------------
/circuits/zkecdsa/src/ecrecover.nr:
--------------------------------------------------------------------------------
1 | use dep::std;
2 |
3 | // credit: https://github.com/colinnielsen/ecrecover-noir/tree/main/src
4 |
5 | fn ecrecover(
6 | pub_key_x: [u8; 32],
7 | pub_key_y: [u8; 32],
8 | signature: [u8; 64], // clip v value
9 | hashed_message: [u8; 32]
10 | ) -> pub Field {
11 | assert(verify_sig(pub_key_x, pub_key_y, signature, hashed_message) == true);
12 | let addr = to_eth_address(pub_key_x, pub_key_y);
13 | addr
14 | }
15 |
16 | fn verify_sig(pub_key_x: [u8; 32], pub_key_y: [u8; 32], signature: [u8; 64], hashed_message: [u8; 32]) -> bool {
17 | let isValid = std::ecdsa_secp256k1::verify_signature(pub_key_x, pub_key_y, signature, hashed_message);
18 | isValid == 1
19 | }
20 |
21 | fn to_eth_address(pub_key_x: [u8; 32], pub_key_y: [u8; 32]) -> Field {
22 | let pub_key = unify_pub_x_pub_y(pub_key_x, pub_key_y);
23 | let hashed_pub_key = std::hash::keccak256(pub_key);
24 |
25 | u8_32_to_u160(hashed_pub_key)
26 | }
27 |
28 | fn unify_pub_x_pub_y(array_x: [u8; 32], array_y: [u8; 32]) -> [u8; 64] {
29 | let mut combined: [u8; 64] = [0; 64];
30 |
31 | for i in 0..32 {
32 | combined[i] = array_x[i];
33 | }
34 | for i in 0..32 {
35 | combined[i + 32] = array_y[i];
36 | }
37 | combined
38 | }
39 |
40 | fn u8_32_to_u160(array: [u8; 32]) -> Field {
41 | let mut addr: Field = 0;
42 |
43 | for i in 0..20 {
44 | // only take the last 20 bytes of the hash
45 | addr = (addr * 256) + (array[i + 12] as Field);
46 | }
47 |
48 | addr
49 | }
50 |
--------------------------------------------------------------------------------
/script/Deploy4337.s.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity 0.8.12;
3 |
4 | import "forge-std/Script.sol";
5 | // import "forge-std/Vm.sol";
6 | // import "forge-std/console.sol";
7 |
8 | import "src/erc4337/Account.sol";
9 | import "src/erc4337/AccountFactory.sol";
10 | import "src/verifier/UltraVerifier.sol";
11 | // import "src/core/NonTransparentProxy.sol";
12 | import "../test/utils/pubkey/address.sol";
13 |
14 | import {EntryPoint} from "account-abstraction/core/EntryPoint.sol";
15 |
16 | contract DeployAccount is Script, Addresses {
17 | address deployerAddress = vm.envAddress("ADDRESS");
18 | uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
19 |
20 | function run() external {
21 | vm.startBroadcast(deployerPrivateKey);
22 |
23 | EntryPoint entryPoint = EntryPoint(
24 | payable(0x0576a174D229E3cFA37253523E645A78A0C91B57)
25 | );
26 | AccountFactory factory = new AccountFactory(entryPoint);
27 |
28 | UltraVerifier verifier = new UltraVerifier();
29 |
30 | // forge script script/Deploy4337.s.sol:DeployAccount --broadcast
31 |
32 | bytes32[] memory guardians = new bytes32[](3);
33 | guardians[0] = hashedAddr[0];
34 | guardians[1] = hashedAddr[1];
35 | guardians[2] = hashedAddr[2];
36 |
37 | ZkECDSAA ret = factory.createAccount(
38 | hashedAddr[0],
39 | address(verifier),
40 | guardians,
41 | uint8(2),
42 | 12 weeks, // a day
43 | hashedAddr[4],
44 | 0
45 | );
46 |
47 | console.logAddress(address(ret));
48 |
49 | vm.stopBroadcast();
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/circuits/zkecdsa/Verifier.toml:
--------------------------------------------------------------------------------
1 | hashedAddr = "0x2cc5a2e55e9a482940665b3e3dc88a9e3b0cf90e6b4966f97c8b13fa686cfe5d"
2 | message_hash = ["0x0000000000000000000000000000000000000000000000000000000000000017", "0x0000000000000000000000000000000000000000000000000000000000000047", "0x00000000000000000000000000000000000000000000000000000000000000e5", "0x0000000000000000000000000000000000000000000000000000000000000014", "0x000000000000000000000000000000000000000000000000000000000000009f", "0x00000000000000000000000000000000000000000000000000000000000000d6", "0x000000000000000000000000000000000000000000000000000000000000002a", "0x0000000000000000000000000000000000000000000000000000000000000046", "0x0000000000000000000000000000000000000000000000000000000000000066", "0x0000000000000000000000000000000000000000000000000000000000000069", "0x0000000000000000000000000000000000000000000000000000000000000076", "0x000000000000000000000000000000000000000000000000000000000000005a", "0x00000000000000000000000000000000000000000000000000000000000000cd", "0x0000000000000000000000000000000000000000000000000000000000000052", "0x000000000000000000000000000000000000000000000000000000000000001b", "0x00000000000000000000000000000000000000000000000000000000000000b6", "0x00000000000000000000000000000000000000000000000000000000000000d7", "0x0000000000000000000000000000000000000000000000000000000000000039", "0x0000000000000000000000000000000000000000000000000000000000000010", "0x00000000000000000000000000000000000000000000000000000000000000e2", "0x00000000000000000000000000000000000000000000000000000000000000d4", "0x0000000000000000000000000000000000000000000000000000000000000021", "0x000000000000000000000000000000000000000000000000000000000000007e", "0x0000000000000000000000000000000000000000000000000000000000000078", "0x00000000000000000000000000000000000000000000000000000000000000fc", "0x0000000000000000000000000000000000000000000000000000000000000017", "0x00000000000000000000000000000000000000000000000000000000000000b2", "0x0000000000000000000000000000000000000000000000000000000000000068", "0x0000000000000000000000000000000000000000000000000000000000000045", "0x00000000000000000000000000000000000000000000000000000000000000ae", "0x0000000000000000000000000000000000000000000000000000000000000005", "0x000000000000000000000000000000000000000000000000000000000000005c"]
3 |
--------------------------------------------------------------------------------
/circuits/hash/src/main.nr:
--------------------------------------------------------------------------------
1 | use dep::std;
2 |
3 | // fn main(address : pub Field, salt: Field) {
4 |
5 | // let mut owner: [Field; 2] = [0; 2];
6 | // owner[0] = address;
7 | // owner[1] = salt;
8 | // let ownerHash = std::hash::pedersen(owner);
9 | // std::println(ownerHash);
10 | // }
11 |
12 | // fn main(address : pub Field) {
13 | // let mut owner: [Field; 1] = [0; 1];
14 | // owner[0] = address;
15 | // let ownerHash = std::hash::pedersen(owner);
16 | // std::println(ownerHash);
17 | // }
18 |
19 | fn main(addresses : pub [Field; 5]) {
20 |
21 | for i in 0..5 {
22 | let mut owner: [Field; 1] = [0; 1];
23 | owner[0] = addresses[i];
24 | let hashedAdd = std::hash::pedersen(owner);
25 | std::println(hashedAdd)
26 | }
27 |
28 | // owner/g1: 0x13ab2733d03b0c89ab8222acd18b002120fec289a54d5769536b3b758d8dc780
29 | // g2: 0x1c9fcea8cdb14e79710ec43e3f5a2c0b5c736586bd4a896c52033acab345577d
30 | // g3: 0x2cc5a2e55e9a482940665b3e3dc88a9e3b0cf90e6b4966f97c8b13fa686cfe5d
31 | // new owner: 0x1c02f61c7c5e6510aeee895347bd0ed9d1162d7038a33a364f65d511b2436d80
32 | // beneficiary: 0x108206375df5b41c4d706cd9d4715c38781d22ee9ff66a5cb887bbec403dea74
33 | }
34 |
35 | #[test]
36 | fn test_main() {
37 | // main(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266, 0);
38 | // 0x2840920c6b28172affa5533dbcec73f20e1a7d54cdb0c5d79b1297895c3c6d03
39 | // main(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266);
40 | //0x13ab2733d03b0c89ab8222acd18b002120fec289a54d5769536b3b758d8dc780
41 |
42 | let addresses: [Field; 5] = [
43 | 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266,
44 | 0x70997970C51812dc3A010C7d01b50e0d17dc79C8,
45 | 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC,
46 | 0x90F79bf6EB2c4f870365E785982E1f101E93b906,
47 | 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65
48 | ];
49 |
50 | // 0x13ab2733d03b0c89ab8222acd18b002120fec289a54d5769536b3b758d8dc780
51 | // 0x1c9fcea8cdb14e79710ec43e3f5a2c0b5c736586bd4a896c52033acab345577d
52 | // 0x2cc5a2e55e9a482940665b3e3dc88a9e3b0cf90e6b4966f97c8b13fa686cfe5d
53 | // 0x1c02f61c7c5e6510aeee895347bd0ed9d1162d7038a33a364f65d511b2436d80
54 | // 0x108206375df5b41c4d706cd9d4715c38781d22ee9ff66a5cb887bbec403dea74
55 |
56 | main(addresses);
57 |
58 |
59 | // Uncomment to make test fail
60 | // main(1, 1);
61 | }
62 |
--------------------------------------------------------------------------------
/circuits/zkecdsa/src/array.nr:
--------------------------------------------------------------------------------
1 |
2 | use dep::std;
3 |
4 | fn to_first_32(array_x: [u8; 64]) -> [u8; 32] {
5 | let mut combined: [u8; 32] = [0; 32];
6 |
7 | for i in 0..32 {
8 | combined[i] = array_x[i];
9 | }
10 |
11 | combined
12 | }
13 |
14 | fn to_second_32(array_x: [u8; 64]) -> [u8; 32] {
15 | let mut combined: [u8; 32] = [0; 32];
16 | let mut j = 0;
17 | for i in 0..32 {
18 | j = i + 32;
19 | //std::println(j);
20 | combined[i] = array_x[j];
21 | }
22 | combined
23 | }
24 |
25 | fn to_first_64(array_x: [u8; 128]) -> [u8; 64] {
26 | let mut combined: [u8; 64] = [0; 64];
27 |
28 | for i in 0..64 {
29 | combined[i] = array_x[i];
30 | }
31 |
32 | combined
33 | }
34 |
35 | fn to_second_64(array_x: [u8; 128]) -> [u8; 64] {
36 | let mut combined: [u8; 64] = [0; 64];
37 | let mut j = 0;
38 | for i in 0..64 {
39 | j = i + 64;
40 | combined[i] = array_x[j];
41 | }
42 | combined
43 | }
44 |
45 | #[test]
46 | fn test_to_first_32() {
47 | let ret = to_first_32(TEST_ARRAY_64);
48 |
49 | // 141
50 | let tenth = ret[10];
51 | std::println(tenth);
52 | }
53 |
54 | #[test]
55 | fn test_to_second_32() {
56 | let ret = to_second_32(TEST_ARRAY_64);
57 |
58 | // 38
59 | let tenth = ret[10];
60 | std::println(tenth);
61 | }
62 |
63 | #[test]
64 | fn test_to_first_64() {
65 | let ret = to_first_64(TEST_ARRAY_128);
66 |
67 | let zero = ret[0]; // 118
68 | std::println(zero);
69 | let thirty3 = ret[32]; // 50
70 | std::println(thirty3);
71 | }
72 |
73 |
74 | #[test]
75 | fn test_to_second_64() {
76 | let ret = to_second_64(TEST_ARRAY_128);
77 |
78 | let zero = ret[0]; // 50
79 | std::println(zero);
80 | let thirty3 = ret[32]; // 118
81 | std::println(thirty3);
82 | }
83 |
84 |
85 |
86 | global TEST_ARRAY_64: [u8;64]
87 | = [
88 | 118, 190, 21, 249, 139, 28, 162, 171, 252, 167, 234, 29, 188, 180, 82, 241, 91, 191, 193, 206, 15, 102, 35, 4, 79, 76, 69, 27, 27, 191, 31, 128,
89 | 50, 49, 102, 29, 255, 107, 87, 223, 119, 141, 38, 132, 19, 105, 246, 167, 216, 172, 148, 34, 60, 43, 97, 141, 11, 223, 40, 120, 196, 61, 42, 79
90 | ];
91 |
92 | global TEST_ARRAY_128: [u8;128]
93 | = [
94 | 118, 190, 21, 249, 139, 28, 162, 171, 252, 167, 234, 29, 188, 180, 82, 241, 91, 191, 193, 206, 15, 102, 35, 4, 79, 76, 69, 27, 27, 191, 31, 128,
95 | 50, 49, 102, 29, 255, 107, 87, 223, 119, 141, 38, 132, 19, 105, 246, 167, 216, 172, 148, 34, 60, 43, 97, 141, 11, 223, 40, 120, 196, 61, 42, 79,
96 | 50, 49, 102, 29, 255, 107, 87, 223, 119, 141, 38, 132, 19, 105, 246, 167, 216, 172, 148, 34, 60, 43, 97, 141, 11, 223, 40, 120, 196, 61, 42, 79,
97 | 118, 190, 21, 249, 139, 28, 162, 171, 252, 167, 234, 29, 188, 180, 82, 241, 91, 191, 193, 206, 15, 102, 35, 4, 79, 76, 69, 27, 27, 191, 31, 128,
98 | ];
--------------------------------------------------------------------------------
/circuits/zkecdsa/src/main.nr:
--------------------------------------------------------------------------------
1 | use dep::std;
2 | mod array;
3 | mod ecrecover;
4 |
5 | fn main(
6 | hashedAddr: pub Field,
7 | pub_key: [u8; 64],
8 | signature: [u8; 64],
9 | message_hash: pub [u8; 32],
10 | ) {
11 |
12 | let pubkey_x = array::to_first_32(pub_key);
13 | let pubkey_y = array::to_second_32(pub_key);
14 |
15 | let recovered_addr = ecrecover::ecrecover(
16 | pubkey_x,
17 | pubkey_y,
18 | signature,
19 | message_hash
20 | );
21 |
22 | std::println(recovered_addr);
23 |
24 | let mut addr: [Field; 1] = [0; 1];
25 | addr[0] = recovered_addr;
26 |
27 | let computed_root = std::hash::pedersen(addr);
28 | std::println(computed_root);
29 | std::println(hashedAddr);
30 |
31 | assert(computed_root[0] == hashedAddr);
32 | }
33 |
34 | #[test]
35 | fn test_main() {
36 |
37 | // is this userOp hash or not...?
38 | // let message_hash = [244, 76, 64, 128, 236, 209, 131, 235, 199, 196, 208, 190, 254, 189, 11, 114, 21, 72, 62, 62, 228, 167, 27, 214, 176, 254, 219, 48, 156, 187, 175, 109];
39 |
40 | // let pub_key = [
41 | // 131, 24, 83, 91, 84, 16, 93, 74, 122, 174, 96,
42 | // 192, 143, 196, 95, 150, 135, 24, 27, 79, 223, 198,
43 | // 37, 189, 26, 117, 63, 167, 57, 127, 237, 117, 53,
44 | // 71, 241, 28, 168, 105, 102, 70, 242, 243, 172, 176,
45 | // 142, 49, 1, 106, 250, 194, 62, 99, 12, 93, 17,
46 | // 245, 159, 97, 254, 245, 123, 13, 42, 165
47 | // ];
48 |
49 | // let hashedAddr = 0x2840920c6b28172affa5533dbcec73f20e1a7d54cdb0c5d79b1297895c3c6d03;
50 | // let signature = [
51 | // 247, 102, 238, 229, 166, 185, 153, 196, 118, 89, 130,
52 | // 230, 55, 15, 25, 236, 56, 94, 220, 16, 31, 116,
53 | // 241, 51, 79, 0, 193, 235, 139, 77, 35, 69, 53,
54 | // 132, 190, 0, 222, 52, 35, 222, 81, 28, 183, 137,
55 | // 135, 149, 66, 130, 181, 149, 17, 115, 207, 145, 248,
56 | // 198, 41, 190, 204, 133, 55, 38, 48, 144
57 | // ];
58 |
59 | let message_hash = [
60 | 25, 207, 171, 174, 102, 123, 180,
61 | 203, 237, 218, 232, 136, 111, 180,
62 | 142, 11, 68, 251, 9, 3, 144,
63 | 103, 41, 137, 231, 122, 0, 249,
64 | 220, 242, 224, 153
65 | ];
66 |
67 | let pub_key = [
68 | 131, 24, 83, 91, 84, 16, 93, 74, 122, 174, 96,
69 | 192, 143, 196, 95, 150, 135, 24, 27, 79, 223, 198,
70 | 37, 189, 26, 117, 63, 167, 57, 127, 237, 117, 53,
71 | 71, 241, 28, 168, 105, 102, 70, 242, 243, 172, 176,
72 | 142, 49, 1, 106, 250, 194, 62, 99, 12, 93, 17,
73 | 245, 159, 97, 254, 245, 123, 13, 42, 165
74 | ];
75 |
76 | let hashedAddr = 0x2840920c6b28172affa5533dbcec73f20e1a7d54cdb0c5d79b1297895c3c6d03;
77 | let signature = [
78 | 210, 1, 118, 62, 14, 109, 127, 105, 12, 114, 199,
79 | 141, 95, 204, 184, 88, 127, 81, 34, 240, 134, 36,
80 | 251, 19, 217, 164, 34, 38, 215, 195, 248, 181, 56,
81 | 80, 60, 135, 183, 77, 233, 227, 170, 10, 94, 17,
82 | 48, 11, 13, 171, 88, 69, 93, 69, 114, 18, 223,
83 | 172, 226, 73, 196, 117, 252, 172, 3, 74
84 | ];
85 |
86 |
87 | main(hashedAddr, pub_key, signature, message_hash);
88 | // std::println(ret);
89 | // assert(ret == true);
90 | }
91 |
--------------------------------------------------------------------------------
/test/utils/BytesLib.sol:
--------------------------------------------------------------------------------
1 | pragma solidity ^0.8.0;
2 |
3 | library BytesLib {
4 | function decodeRecoveryArgs(
5 | bytes memory _calldata
6 | ) internal pure returns (bytes32) {
7 | bytes memory data = extractCalldata(_calldata);
8 | (bytes32 newOwner, bytes32 guradian) = abi.decode(
9 | data,
10 | (bytes32, bytes32)
11 | );
12 | return guradian;
13 | }
14 |
15 | function decodeProposeInheritanceArgs(
16 | bytes memory _calldata
17 | ) internal pure returns (bytes32) {
18 | bytes memory data = extractCalldata(_calldata);
19 | bytes32 beneficiary = abi.decode(data, (bytes32));
20 | return beneficiary;
21 | }
22 |
23 | function decodeExecuteInheritanceArgs(
24 | bytes memory _calldata
25 | ) internal pure returns (bytes32) {
26 | bytes memory data = extractCalldata(_calldata);
27 | (bytes32 beneficiary, uint inheritanceCount) = abi.decode(
28 | data,
29 | (bytes32, uint)
30 | );
31 | return beneficiary;
32 | }
33 |
34 | function extractCalldata(
35 | bytes memory _calldata
36 | ) internal pure returns (bytes memory) {
37 | bytes memory data;
38 |
39 | require(_calldata.length >= 4);
40 |
41 | assembly {
42 | let totalLength := mload(_calldata)
43 | let targetLength := sub(totalLength, 4)
44 | data := mload(0x40)
45 |
46 | mstore(data, targetLength)
47 | mstore(0x40, add(0x20, targetLength))
48 | mstore(add(data, 0x20), shl(0x20, mload(add(_calldata, 0x20))))
49 |
50 | for {
51 | let i := 0x1C
52 | } lt(i, targetLength) {
53 | i := add(i, 0x20)
54 | } {
55 | mstore(
56 | add(add(data, 0x20), i),
57 | mload(add(add(_calldata, 0x20), add(i, 0x04)))
58 | )
59 | }
60 | }
61 |
62 | return data;
63 | }
64 |
65 | function getSelector(bytes memory _data) internal pure returns (bytes4) {
66 | bytes4 selector;
67 | assembly {
68 | selector := calldataload(_data)
69 | }
70 | return selector;
71 | }
72 |
73 | // chat gpt
74 | function bytes32ToUint8Array(
75 | bytes32 b
76 | ) public pure returns (uint8[] memory) {
77 | uint8[] memory array = new uint8[](32);
78 | for (uint i = 0; i < 32; i++) {
79 | array[i] = uint8(b[i]);
80 | }
81 | return array;
82 | }
83 |
84 | function bytesToUint8Array(
85 | bytes memory input
86 | ) public pure returns (uint8[] memory) {
87 | uint8[] memory array = new uint8[](input.length);
88 | for (uint i = 0; i < input.length; i++) {
89 | array[i] = uint8(input[i]);
90 | }
91 | return array;
92 | }
93 |
94 | function sliceStartEnd(
95 | bytes memory data,
96 | uint start,
97 | uint end
98 | ) public pure returns (bytes memory) {
99 | require(start <= end && end <= data.length, "Invalid range");
100 |
101 | bytes memory result = new bytes(end - start);
102 | for (uint i = start; i < end; i++) {
103 | result[i - start] = data[i];
104 | }
105 | return result;
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/modules/Recovery.sol:
--------------------------------------------------------------------------------
1 | pragma solidity ^0.8.12;
2 |
3 | import "./ModuleBase.sol";
4 |
5 | // https://github.com/noir-lang/noir-starter/blob/main/foundry-voting/src/zkVote.sol
6 | // https://github.com/colinnielsen/dark-safe/blob/colinnielsen/verify-sigs/contracts/DarkSafe.sol
7 |
8 | abstract contract RecoveryModule is ModuleBase {
9 | mapping(bytes32 => bool) public guardians;
10 |
11 | struct Recovery {
12 | uint approvalCount;
13 | uint deadline;
14 | mapping(bytes32 => bool) voted; // double-spend checker
15 | }
16 |
17 | mapping(bytes32 => Recovery) public recoveries; // new owner => Recovery
18 |
19 | uint8 public recoveryThreshold;
20 |
21 | // ) public onlySelf {
22 | function initializeRecovery(
23 | bytes32[] memory _guardians,
24 | uint8 _threshold
25 | ) internal {
26 | require(_guardians.length >= _threshold, "INVALID_GUARDIAN_NUMBER");
27 | setGurdian(_guardians);
28 | setThreshold(_threshold);
29 | }
30 |
31 | function setGurdian(bytes32[] memory _guardians) public {
32 | for (uint i; i < _guardians.length; i++) {
33 | guardians[_guardians[i]] = true;
34 | }
35 | }
36 |
37 | function removeGurdian(bytes32[] memory _guardians) external onlySelf {
38 | for (uint i; i < _guardians.length; i++) {
39 | guardians[_guardians[i]] = false;
40 | }
41 | }
42 |
43 | function setThreshold(uint8 _threshold) public {
44 | recoveryThreshold = _threshold;
45 | }
46 |
47 | // called from new EOA which is created by the owner who lost access to this account
48 | function proposeRecovery(bytes32 _newOwner, uint _deadline) public {
49 | require(_newOwner != _owmer(), "INVALID_NEW_OWENR");
50 | require(_newOwner.length != 0, "INVALID_BYTES");
51 |
52 | Recovery storage recovery = recoveries[_newOwner];
53 | recovery.approvalCount = 0;
54 | recovery.deadline = _deadline;
55 | }
56 |
57 | function approveRecovery(
58 | bytes32 _newOwner, // proposed new onwer
59 | bytes32 _guardian // "caller"
60 | ) public onlySelf {
61 | require(
62 | block.timestamp < recoveries[_newOwner].deadline,
63 | "Voting period is over."
64 | );
65 | require(
66 | !recoveries[_newOwner].voted[_guardian],
67 | "DOUBLE_VOTE_NOT_ALLOWED"
68 | );
69 | require(guardians[_guardian], "INVALID_GUARDIAN");
70 | recoveries[_newOwner].approvalCount += 1;
71 | if (recoveries[_newOwner].approvalCount >= recoveryThreshold) {
72 | changeOwner(_newOwner);
73 | }
74 | recoveries[_newOwner].voted[_guardian] = true;
75 | }
76 |
77 | function getApprovalCount(bytes32 _hashedAddr) public view returns (uint) {
78 | return recoveries[_hashedAddr].approvalCount;
79 | }
80 |
81 | function getVoted(
82 | bytes32 _newOwner,
83 | bytes32 _approver
84 | ) public view returns (bool) {
85 | return recoveries[_newOwner].voted[_approver];
86 | }
87 | }
88 |
89 | /*
90 |
91 | // -- this part below can be put into validateSignature ---
92 | // implementing all features with one circuit.
93 |
94 | bytes32[] memory publicInputs = new bytes32[](3);
95 | publicInputs[0] = _guardian;
96 | publicInputs[1] = msg.data;
97 |
98 | // --- ----
99 | */
100 |
--------------------------------------------------------------------------------
/test/utils/NoirHelper.sol:
--------------------------------------------------------------------------------
1 | pragma solidity 0.8.12;
2 |
3 | // import {TestBase} from "forge-std/Base.sol";
4 | import "forge-std/Test.sol";
5 | import "@openzeppelin/contracts/utils/Strings.sol";
6 |
7 | contract NoirHelper is Test {
8 | using Strings for uint;
9 | struct CircuitInput {
10 | string n_hashedAddr;
11 | bytes32 hashedAddr;
12 | string n_pub_key;
13 | uint8[] pub_key;
14 | string n_signature;
15 | uint8[] signature;
16 | string n_message_hash;
17 | uint8[] message_hash;
18 | }
19 |
20 | CircuitInput public inputs;
21 |
22 | function withInput(
23 | string memory n_hashedAddr,
24 | bytes32 hashedAddr,
25 | string memory n_pub_key,
26 | uint8[] memory pub_key,
27 | string memory n_signature,
28 | uint8[] memory signature,
29 | string memory n_message_hash,
30 | uint8[] memory message_hash
31 | ) public returns (NoirHelper) {
32 | inputs = CircuitInput(
33 | n_hashedAddr,
34 | hashedAddr,
35 | n_pub_key,
36 | pub_key,
37 | n_signature,
38 | signature,
39 | n_message_hash,
40 | message_hash
41 | );
42 | return this;
43 | }
44 |
45 | function clean() public {
46 | string[] memory ffi_cmds = new string[](1);
47 | ffi_cmds[0] = "./delete.sh";
48 | vm.ffi(ffi_cmds);
49 | delete inputs;
50 | }
51 |
52 | // function readProof(string memory fileName) public returns (bytes memory) {
53 | // string memory file = vm.readFile(
54 | // string.concat("circuits/zkecdsa/proofs/", fileName, ".proof")
55 | // );
56 | // return vm.parseBytes(file);
57 | // }
58 |
59 | function generateProof(
60 | string memory _path,
61 | string memory _proof_name
62 | ) public returns (bytes memory) {
63 | string memory proverTOML = "circuits/zkecdsa/Prover.toml";
64 | string memory params1 = string.concat(
65 | inputs.n_hashedAddr,
66 | " = ",
67 | '"',
68 | vm.toString(inputs.hashedAddr),
69 | '"'
70 | );
71 |
72 | string memory params2 = string.concat(
73 | inputs.n_pub_key,
74 | " = ",
75 | uint8ArrayToString(inputs.pub_key)
76 | );
77 |
78 | string memory params3 = string.concat(
79 | inputs.n_signature,
80 | " = ",
81 | uint8ArrayToString(inputs.signature)
82 | );
83 |
84 | string memory params4 = string.concat(
85 | inputs.n_message_hash,
86 | " = ",
87 | uint8ArrayToString(inputs.message_hash)
88 | );
89 |
90 | vm.writeLine(proverTOML, params1);
91 | vm.writeLine(proverTOML, params2);
92 | vm.writeLine(proverTOML, params3);
93 | vm.writeLine(proverTOML, params4);
94 |
95 | // generate proof
96 | string[] memory ffi_cmds = new string[](1);
97 | ffi_cmds[0] = string.concat("./prove/prove_", _proof_name, ".sh");
98 | // chmod +x ./prove.sh to give the permission.
99 | vm.ffi(ffi_cmds);
100 |
101 | // clean inputs
102 | clean();
103 | // read proof
104 | string memory proof = vm.readFile(_path); // "circuits/zkecdsa/proofs/.proof"
105 | return vm.parseBytes(proof);
106 | }
107 |
108 | function uint8ArrayToString(
109 | uint8[] memory array
110 | ) internal pure returns (string memory) {
111 | string memory str = "[";
112 | for (uint i = 0; i < array.length; i++) {
113 | str = string(abi.encodePacked(str, uint256(array[i]).toString()));
114 | if (i < array.length - 1) {
115 | str = string(abi.encodePacked(str, ","));
116 | }
117 | }
118 | str = string(abi.encodePacked(str, "]"));
119 | return str;
120 | }
121 | }
122 |
123 | // 161b
124 |
--------------------------------------------------------------------------------
/src/erc4337/AccountFactory.sol:
--------------------------------------------------------------------------------
1 | //SPDX-License-Identifier: UNLICENSED
2 | pragma solidity ^0.8.12;
3 |
4 | import "@openzeppelin/contracts/utils/Create2.sol";
5 | import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
6 | import "./Account.sol";
7 |
8 | /**
9 | * A sample factory contract for SimpleAccount
10 | * A UserOperations "initCode" holds the address of the factory, and a method call (to createAccount, in this sample factory).
11 | * The factory's createAccount returns the target account address even if it is already installed.
12 | * This way, the entryPoint.getSenderAddress() can be called either before or after the account is created.
13 | */
14 | contract AccountFactory {
15 | ZkECDSAA public immutable accountImplementation;
16 |
17 | constructor(IEntryPoint _entryPoint) {
18 | accountImplementation = new ZkECDSAA(address(_entryPoint));
19 | }
20 |
21 | /**
22 | * create an account, and return its address.
23 | * returns the address even if the account is already deployed.
24 | * Note that during UserOperation execution, this method is called only if the account is not deployed.
25 | * This method returns an existing account address so that entryPoint.getSenderAddress() would work even after account creation
26 | */
27 | function createAccount(
28 | bytes32 _owner,
29 | address _verifier,
30 | bytes32[] memory _guardians,
31 | uint8 _threshold,
32 | uint _pendingPeriod,
33 | bytes32 _beneficiary,
34 | uint256 salt
35 | ) public returns (ZkECDSAA ret) {
36 | address addr = getAddress(
37 | _owner,
38 | _verifier,
39 | _guardians,
40 | _threshold,
41 | _pendingPeriod,
42 | _beneficiary,
43 | salt
44 | );
45 | uint codeSize = addr.code.length;
46 | if (codeSize > 0) {
47 | return ZkECDSAA(payable(addr));
48 | }
49 | ret = ZkECDSAA(
50 | payable(
51 | new ERC1967Proxy{salt: bytes32(salt)}(
52 | address(accountImplementation),
53 | abi.encodeCall(
54 | ZkECDSAA.initialize,
55 | (
56 | _owner,
57 | _verifier,
58 | _guardians,
59 | _threshold,
60 | _pendingPeriod,
61 | _beneficiary
62 | )
63 | )
64 | )
65 | )
66 | );
67 | }
68 |
69 | /**
70 | * calculate the counterfactual address of this account as it would be returned by createAccount()
71 | */
72 | function getAddress(
73 | bytes32 _owner,
74 | address _verifier,
75 | bytes32[] memory _guardians,
76 | uint8 _threshold,
77 | uint _pendingPeriod,
78 | bytes32 _beneficiary,
79 | uint256 salt
80 | ) public view returns (address) {
81 | return
82 | Create2.computeAddress(
83 | bytes32(salt),
84 | keccak256(
85 | abi.encodePacked(
86 | type(ERC1967Proxy).creationCode,
87 | abi.encode(
88 | address(accountImplementation),
89 | abi.encodeCall(
90 | ZkECDSAA.initialize,
91 | (
92 | _owner,
93 | _verifier,
94 | _guardians,
95 | _threshold,
96 | _pendingPeriod,
97 | _beneficiary
98 | )
99 | )
100 | )
101 | )
102 | )
103 | );
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/modules/Inheritance.sol:
--------------------------------------------------------------------------------
1 | pragma solidity ^0.8.12;
2 |
3 | import "./ModuleBase.sol";
4 |
5 | abstract contract InheritanceModule is ModuleBase {
6 | uint public pendingPeriod; // e.g. 7884008 ( = 3 months )
7 |
8 | struct Inheritance {
9 | bytes32 beneficiary;
10 | bool succeed;
11 | uint deadline; // time until when the current owner should reject the proposal
12 | }
13 |
14 | mapping(uint => Inheritance) public inheritances; // inheritanceCount => Inheritance
15 | uint public inheritanceCount;
16 |
17 | mapping(bytes32 => bool) public beneficiaries;
18 |
19 | function initializeInheritance(
20 | uint _pendingPeriod,
21 | bytes32 _beneficiary
22 | ) internal {
23 | setPendingPeriod(_pendingPeriod);
24 | setBeneficiary(_beneficiary);
25 | }
26 |
27 | function setPendingPeriod(uint _pendingPeriod) public {
28 | pendingPeriod = _pendingPeriod;
29 | }
30 |
31 | function setBeneficiary(bytes32 _beneficiary) public {
32 | beneficiaries[_beneficiary] = true;
33 | }
34 |
35 | function proposeInheritance(bytes32 _beneficiary) public returns (uint) {
36 | require(_beneficiary.length != 0, "INVALID_BENFICIARY_LENGTH");
37 | require(_beneficiary != _owmer(), "INVALID_BENFICIARY");
38 | require(beneficiaries[_beneficiary], "NON_REGISTERED_BENFICIARY");
39 |
40 | uint newInheritanceCount = inheritanceCount + 1;
41 |
42 | Inheritance memory inheritance = inheritances[newInheritanceCount];
43 | inheritance.beneficiary = _beneficiary;
44 | inheritance.succeed = true;
45 | inheritance.deadline = block.timestamp + pendingPeriod;
46 |
47 | inheritances[newInheritanceCount] = inheritance;
48 |
49 | return newInheritanceCount;
50 | }
51 |
52 | // if owner doesn't reject the proposal && deadline has been passed, the proposer can take ownership.
53 | // if owner said no, it fails.
54 | function executeInheritance(
55 | bytes32 _beneficiary,
56 | uint _inheritanceCount
57 | ) public {
58 | Inheritance memory inheritance = inheritances[_inheritanceCount];
59 | require(_beneficiary == inheritance.beneficiary, "INVALID_BENFICIARY");
60 |
61 | if (inheritance.succeed && inheritance.deadline <= block.timestamp) {
62 | bytes32 newOwner = inheritance.beneficiary;
63 | changeOwner(newOwner);
64 | } else {
65 | revert("INHERITRANCE_FAILED");
66 | }
67 | }
68 |
69 | // the owner approves the change of the ownership without waiting for pending period.
70 | // if expired, the owner can't do this but wait the proposer to execute inheritance.
71 | function approveInheritance(uint _inheritanceCount) public {
72 | require(
73 | _inheritanceCount != 0 && _inheritanceCount <= inheritanceCount,
74 | "INVALID_COUNT"
75 | );
76 | Inheritance memory inheritance = inheritances[_inheritanceCount];
77 | require(inheritance.succeed, "INVALID_ACTION");
78 | require(inheritance.deadline <= block.timestamp, "EXPIERED_ACTION");
79 | bytes32 newOwner = inheritance.beneficiary;
80 | changeOwner(newOwner);
81 | }
82 |
83 | // the owner simply rejects the change of the ownership within the pending period.
84 | // if expired, the owner can't reject but wait the proposer to execute inheritance.
85 | function rejectInheritance(uint _inheritanceCount) public {
86 | require(
87 | _inheritanceCount != 0 && _inheritanceCount <= inheritanceCount,
88 | "INVALID_COUNT"
89 | );
90 | Inheritance memory inheritance = inheritances[_inheritanceCount];
91 | require(inheritance.succeed, "INVALID_ACTION");
92 | require(inheritance.deadline <= block.timestamp, "EXPIERED_ACTION");
93 | inheritance.succeed = false;
94 | inheritances[_inheritanceCount] = inheritance;
95 | }
96 |
97 | function getBeneficiary(uint _count) public view returns (bytes32) {
98 | return inheritances[_count].beneficiary;
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AnonAA
2 |
3 | 
4 |
5 | *This project is built at the ETHPrague hackathon. [Project Page.](https://devfolio.co/projects/anonaa-f675)
6 |
7 | ## Overview
8 |
9 | AnonAA is an ERC4337-based social recovery wallet that conceals guardians' addresses to prevent their potential corruption. It also implements privacy-preserving features like private ownership and private ownership transfer. zk-ecdsa written in Noir, a generalized zkp language built by Aztec, enables these features.
10 |
11 | ## Problems & Solutions
12 |
13 | AnonAA has three distinct privacy features which solve different problems. All of the solutions below are made possible with zk-ecdsa, ecrecover carried out through a zero-knowledge circuit. The zk-proof is verified on-chain. it replaces the ecrecover function which is commonly used in transaction signature verification in smart contract wallets.
14 |
15 | ### Secuirty-enhanced Social Recovery
16 |
17 | One of the biggest unspoken risks associated with the current social recovery scheme is the possible corruption in which the "trusted" guardians communicate behind the scene and collude to take the account ownership and steal the owner's assets.
18 |
19 | Imagine a social recovery wallet with 3 guardians (one is your backup address and the other two are people you trust, like your family members and close friends) and the threshold is 2. As long as the stored guardian addresses are publicly known, it's not difficult for guardians other than you to collude.
20 |
21 | To prevent such actions, AnonAA allows you to store the guardian address masked(hashed) and they can interact with the wallet ( approve/ reject recovery proposals) without revealing their public identity ( eht address / public key ) so that they can't know who the other guardians are, making the corruption nearly impossible.
22 |
23 | ### Private Ownership
24 |
25 | AnonAA only stores encrypted addresses which helps the owner hide the ownership of the account. Hence, as long as the owner manages the account without making any link to his/her other addresses on-chain, nobody can guess/know who controls the smart contract wallet.
26 |
27 | ### Private Inheritance
28 |
29 | AnonAA allows for safe and private transfer of account ownership. Even if you are put into unexpected situations like death and imprisonment where you can't have access to your account/funds anymore, people such as your son, daughter, and wife can safely inherit your assets anonymously.
30 |
31 | ## Technologies
32 |
33 | - [Noir](https://noir-lang.org/), a language for creating and verifying zk-proofs built by Aztec.
34 | - [ERC4337](https://eips.ethereum.org/EIPS/eip-4337), an Account Abstraction standard.
35 |
36 | ### Inspiration & Credit
37 |
38 | - [ecrecover-noir](https://github.com/colinnielsen/ecrecover-noir): ecrecover implementation in Noir.
39 | - [DarkSafe](https://github.com/colinnielsen/dark-safe) : Shielded Safe with Noir ecrecover.
40 | - [nplate](https://github.com/whitenois3/nplate): a template project with Noir proof generation in foundry test.
41 |
42 | ## Deployed Contracts
43 |
44 | Here is the list of the Account contract addresses deployed on each network.
45 |
46 | | Chain | Address |
47 | | --------------- | ------------------------------------------ |
48 | | Goerli | 0x0C92B5E41FBAc2CbF1FAD8D72d5BC4F3f73dA104 |
49 | | Optimism Goerli | 0xaFb4461a934574d33Ae5b759914E14226a3d168e |
50 | | Chiago(Gnosis) | 0x55b89639d847702d948E307B72651D6213efDb7A |
51 | | Scroll alpha | 0x542a0d82F98D1796A38a3382235c98C797eaC4F5 |
52 | | Base Goerli | 0x3a52f22c59bbb86b85eba807cf6ebadbe298d9a3 |
53 |
54 | ## Challenges
55 |
56 | ### Building Frontend
57 |
58 | Since Noir's JS library used for generating zk-proof is unusable as it hasn't been updated to the latest version of Noir, I couldn't build a fron-tend where users can locally generate proof and submit transactions to get his/her actions done.
59 |
60 | ### Hashed Address, not Merkle root
61 |
62 | AnonAA stores Pedersen-hashed addresses in smart contracts that are practically secure enough to preserve the privacy of the users: the owner, the guardians of social recovery, and the beneficiary of the inheritance. Of course, an attacker can brute-force compute all the public addresses to link them to the hash of the addresses above, but adding additional randomness, such as using secret salt for hashing, would make it nearly impossible.
63 |
64 | Anyway, I think that using Merkle tree/root is a more desirable and elegant solution to manage the user's identity secretly and efficiently as it's more difficult to extract leaves from a root and also reduces storage costs as the amount of identity data increases. Though, unfortunately, this is impossible at this point as the Noir JS library is still out of date as I mentioned above.
65 |
66 | ### Proving time and Verifying cost
67 |
68 | Even though applying ZKP to privacy solutions is cool and effective, I think it's still hard to go in production as the proving time is too long ( abt 1.30mins for each ) and verifying the contract consumes tons of gas ( min. ~500k). But I believe that these bottlenecks will be eased and removed as the technologies improve in Noir and its underlying proving system.
69 |
70 | ### Relayer
71 |
72 | To make AnonAA purely private, there needs to be a relayer that can work as a relayer/paymaster so that users don't reveal its on-chain recorsds for paying gas. I couldn't build it whithin this hackathon period but this is the first thing that would be worked on next.
73 |
74 | ## Deployments
75 |
76 | ##### compile
77 |
78 | ```shell
79 | forge compile
80 | ```
81 |
82 | ##### test
83 |
84 | ```shell
85 | chmod +x ./prove/prove_app_r.sh
86 | ```
87 |
88 | ```shell
89 | chmod +x ./delete.sh
90 | ```
91 |
92 | ```shell
93 | forge test --contracts zkECDSAATest --match-test test_approve_recovery -vv
94 | ```
95 |
96 | Expected outcome would look like the below
97 |
98 |
99 |
100 | ##### deploy
101 |
102 | ```shell
103 | forge script script/Deploy4337.s.sol:DeployAccount --rpc-url --broadcast
104 | ```
105 |
--------------------------------------------------------------------------------
/src/erc4337/Account.sol:
--------------------------------------------------------------------------------
1 | //SPDX-License-Identifier: UNLICENSED
2 | pragma solidity ^0.8.12;
3 |
4 | import "@openzeppelin/contracts/proxy/utils/Initializable.sol";
5 | import "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";
6 | import "account-abstraction/core/BaseAccount.sol";
7 | import "../verifier/UltraVerifier.sol";
8 |
9 | import "../modules/Inheritance.sol";
10 | import "../modules/Recovery.sol";
11 |
12 | /*
13 |
14 | zkECDSAA: An ERC4337 AA wallet with a couple of privacy-preserving features enabled by Noir's zkECDSA.
15 |
16 | features
17 | 1: private social recovery
18 | 2: private owner
19 | 3: private inheritance
20 |
21 | - 1
22 | you can register private guardians for recoverying the ownership in case u lost the access to this acc.
23 | to prevent the corruption of ur guardians, their addresses are hidden.
24 |
25 | - 2
26 | as if u create a new EOA, u can create AA acc which is completely unrelated to ur other addresses
27 | but still one of ur public eth address can control this acc privately.
28 |
29 | - 3
30 | you can privately transfer the ownership this AA acc to somebody else.
31 | The new owner can also control this acc as you used to do.
32 |
33 | TODO
34 | 0. make everything AA tx. with one zkECDSA circuit.
35 | 1. AA test.
36 | - generate the first 4 bytes: function selector of module methods
37 | - write tests for each module (validate userOps)
38 | 2. think through if the secret_salt is really needed
39 | 3. add zksync imp with foundry
40 |
41 | */
42 |
43 | contract ZkECDSAA is
44 | BaseAccount,
45 | UUPSUpgradeable,
46 | Initializable,
47 | RecoveryModule,
48 | InheritanceModule
49 | {
50 | UltraVerifier public verifier;
51 | bytes32 public owner;
52 |
53 | // IEntryPoint private immutable _entryPoint;
54 | IEntryPoint private immutable _entryPoint;
55 |
56 | event ZKECDSAInitialized(
57 | IEntryPoint indexed entryPoint,
58 | bytes32 indexed owner
59 | );
60 |
61 | modifier onlyEntryPoint() {
62 | require(msg.sender == address(_entryPoint), "only entrypoint");
63 | _;
64 | }
65 |
66 | function _onlySelf() internal view {
67 | require(msg.sender == address(this), "ONLY_SELF");
68 | }
69 |
70 | /// @inheritdoc BaseAccount
71 | function entryPoint() public view virtual override returns (IEntryPoint) {
72 | return _entryPoint;
73 | }
74 |
75 | // solhint-disable-next-line no-empty-blocks
76 | receive() external payable {}
77 |
78 | error PROOF_VERIFICATION_FAILED();
79 |
80 | constructor(address anEntryPoint) {
81 | _entryPoint = IEntryPoint(anEntryPoint);
82 | }
83 |
84 | function initialize(
85 | bytes32 _owner,
86 | address _verifier,
87 | bytes32[] memory _guardians,
88 | uint8 _threshold,
89 | uint _pendingPeriod,
90 | bytes32 _beneficiary
91 | ) public initializer {
92 | owner = _owner;
93 | verifier = UltraVerifier(_verifier);
94 | initializeRecovery(_guardians, _threshold);
95 | initializeInheritance(_pendingPeriod, _beneficiary);
96 | }
97 |
98 | /**
99 | * execute a transaction (called directly from owner, or by entryPoint)
100 | */
101 | function execute(
102 | address dest,
103 | uint256 value,
104 | bytes calldata func
105 | ) external onlyEntryPoint {
106 | _call(dest, value, func);
107 | }
108 |
109 | /**
110 | * execute a sequence of transactions
111 | */
112 | function executeBatch(
113 | address[] calldata dest,
114 | bytes[] calldata func
115 | ) external onlyEntryPoint {
116 | require(dest.length == func.length, "wrong array lengths");
117 | for (uint256 i = 0; i < dest.length; i++) {
118 | _call(dest[i], 0, func[i]);
119 | }
120 | }
121 |
122 | function _call(address target, uint256 value, bytes memory data) internal {
123 | (bool success, bytes memory result) = target.call{value: value}(data);
124 | if (!success) {
125 | assembly {
126 | revert(add(result, 32), mload(result))
127 | }
128 | }
129 | }
130 |
131 | function validateUserOp(
132 | UserOperation calldata userOp,
133 | bytes32 userOpHash,
134 | uint256 missingAccountFunds
135 | ) external override returns (uint256 validationData) {
136 | _requireFromEntryPoint();
137 | validationData = _validateSignature(userOp, userOpHash);
138 | _validateNonce(userOp.nonce);
139 | _payPrefund(missingAccountFunds);
140 | }
141 |
142 | function _validateSignature(
143 | UserOperation calldata userOp,
144 | bytes32 userOpHash
145 | ) internal view override returns (uint256 validationData) {
146 | (bytes32 hashedAddr, bytes memory proof) = abi.decode(
147 | userOp.signature,
148 | (bytes32, bytes)
149 | );
150 |
151 | // depending on calldata sig, the hashed address's validity should be checked.
152 | // otherwise, its crap, inehritance beneficiaries and guardians have the same priviledge as the owner...
153 | // if the hashedAddr == guardian, the selector must be approveRecovery
154 | // if the hashedAddr == beneficiary, the selector must be either proposeInheritance or executeInheritance.
155 |
156 | checkCalldataValidity(hashedAddr, bytes4(userOp.callData));
157 |
158 | bytes32[] memory publicInputs = new bytes32[](33);
159 | publicInputs[0] = hashedAddr;
160 |
161 | bytes memory b = bytes.concat(userOpHash);
162 |
163 | for (uint i = 0; i < b.length; i++) {
164 | publicInputs[i + 1] = bytes32(uint(uint8(b[i])));
165 | }
166 |
167 | // signature == proof ( mb better abi encoded)
168 | if (!verifier.verify(proof, publicInputs))
169 | revert PROOF_VERIFICATION_FAILED();
170 | return 0;
171 | }
172 |
173 | function checkCalldataValidity(
174 | bytes32 _hashedAddr,
175 | bytes4 _selector
176 | ) internal view returns (bool) {
177 | if (guardians[_hashedAddr] && _hashedAddr != owner) {
178 | require(_selector == bytes4(0x4bbfbc38), "INVALID_SELECTOR_G");
179 | } else if (beneficiaries[_hashedAddr] && _hashedAddr != owner) {
180 | require(
181 | _selector == bytes4(0x12264e20) ||
182 | _selector == bytes4(0x52ae0147),
183 | "INVALID_SELECTOR_I"
184 | );
185 | }
186 | }
187 |
188 | /**
189 | * check current account deposit in the entryPoint
190 | */
191 | function getDeposit() public view returns (uint256) {
192 | return entryPoint().balanceOf(address(this));
193 | }
194 |
195 | /**
196 | * deposit more funds for this account in the entryPoint
197 | */
198 | function addDeposit() public payable {
199 | entryPoint().depositTo{value: msg.value}(address(this));
200 | }
201 |
202 | /**
203 | * withdraw value from the account's deposit
204 | * @param withdrawAddress target to send to
205 | * @param amount to withdraw
206 | */
207 | function withdrawDepositTo(
208 | address payable withdrawAddress,
209 | uint256 amount
210 | ) public {
211 | _onlySelf();
212 | entryPoint().withdrawTo(withdrawAddress, amount);
213 | }
214 |
215 | function _authorizeUpgrade(
216 | address newImplementation
217 | ) internal view override {
218 | (newImplementation);
219 | _onlySelf();
220 | }
221 |
222 | function changeOwner(bytes32 _owner) internal override {
223 | owner = _owner;
224 | }
225 |
226 | function _owmer() internal view override returns (bytes32) {
227 | return owner;
228 | }
229 | }
230 |
--------------------------------------------------------------------------------
/test/utils/pubkey/address.sol:
--------------------------------------------------------------------------------
1 | pragma solidity 0.8.12;
2 |
3 | contract Addresses {
4 | address[] public addresses = [
5 | 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266,
6 | 0x70997970C51812dc3A010C7d01b50e0d17dc79C8,
7 | 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC,
8 | 0x90F79bf6EB2c4f870365E785982E1f101E93b906,
9 | 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65,
10 | 0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc,
11 | 0x976EA74026E726554dB657fA54763abd0C3a0aa9,
12 | 0x14dC79964da2C08b23698B3D3cc7Ca32193d9955,
13 | 0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f,
14 | 0xa0Ee7A142d267C1f36714E4a8F75612F20a79720
15 | ];
16 |
17 | // bytes[] public public_key = [
18 |
19 | // ];
20 |
21 | uint[] public private_key = [
22 | 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80,
23 | 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d,
24 | 0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a,
25 | 0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6,
26 | 0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a,
27 | 0x8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba,
28 | 0x92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e,
29 | 0x4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356,
30 | 0xdbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97,
31 | 0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6
32 | ];
33 |
34 | bytes32[] public hashedAddr;
35 |
36 | constructor() {
37 | hashedAddr.push(
38 | 0x13ab2733d03b0c89ab8222acd18b002120fec289a54d5769536b3b758d8dc780
39 | );
40 | hashedAddr.push(
41 | 0x1c9fcea8cdb14e79710ec43e3f5a2c0b5c736586bd4a896c52033acab345577d
42 | );
43 | hashedAddr.push(
44 | 0x2cc5a2e55e9a482940665b3e3dc88a9e3b0cf90e6b4966f97c8b13fa686cfe5d
45 | );
46 | hashedAddr.push(
47 | 0x1c02f61c7c5e6510aeee895347bd0ed9d1162d7038a33a364f65d511b2436d80
48 | );
49 | hashedAddr.push(
50 | 0x108206375df5b41c4d706cd9d4715c38781d22ee9ff66a5cb887bbec403dea74
51 | );
52 | }
53 |
54 | uint8[] public pubkey1 = [
55 | 131,
56 | 24,
57 | 83,
58 | 91,
59 | 84,
60 | 16,
61 | 93,
62 | 74,
63 | 122,
64 | 174,
65 | 96,
66 | 192,
67 | 143,
68 | 196,
69 | 95,
70 | 150,
71 | 135,
72 | 24,
73 | 27,
74 | 79,
75 | 223,
76 | 198,
77 | 37,
78 | 189,
79 | 26,
80 | 117,
81 | 63,
82 | 167,
83 | 57,
84 | 127,
85 | 237,
86 | 117,
87 | 53,
88 | 71,
89 | 241,
90 | 28,
91 | 168,
92 | 105,
93 | 102,
94 | 70,
95 | 242,
96 | 243,
97 | 172,
98 | 176,
99 | 142,
100 | 49,
101 | 1,
102 | 106,
103 | 250,
104 | 194,
105 | 62,
106 | 99,
107 | 12,
108 | 93,
109 | 17,
110 | 245,
111 | 159,
112 | 97,
113 | 254,
114 | 245,
115 | 123,
116 | 13,
117 | 42,
118 | 165
119 | ];
120 |
121 | uint8[] public pubkey2 = [
122 | 186,
123 | 87,
124 | 52,
125 | 216,
126 | 247,
127 | 9,
128 | 23,
129 | 25,
130 | 71,
131 | 30,
132 | 127,
133 | 126,
134 | 214,
135 | 185,
136 | 223,
137 | 23,
138 | 13,
139 | 199,
140 | 12,
141 | 198,
142 | 97,
143 | 202,
144 | 5,
145 | 230,
146 | 136,
147 | 96,
148 | 26,
149 | 217,
150 | 132,
151 | 240,
152 | 104,
153 | 176,
154 | 214,
155 | 115,
156 | 81,
157 | 229,
158 | 240,
159 | 96,
160 | 115,
161 | 9,
162 | 36,
163 | 153,
164 | 51,
165 | 106,
166 | 176,
167 | 131,
168 | 158,
169 | 248,
170 | 165,
171 | 33,
172 | 175,
173 | 211,
174 | 52,
175 | 229,
176 | 56,
177 | 7,
178 | 32,
179 | 95,
180 | 162,
181 | 240,
182 | 142,
183 | 236,
184 | 116,
185 | 244
186 | ];
187 |
188 | uint8[] public pubkey3 = [
189 | 157,
190 | 144,
191 | 49,
192 | 233,
193 | 125,
194 | 215,
195 | 143,
196 | 248,
197 | 193,
198 | 90,
199 | 168,
200 | 105,
201 | 57,
202 | 222,
203 | 155,
204 | 30,
205 | 121,
206 | 16,
207 | 102,
208 | 160,
209 | 34,
210 | 78,
211 | 51,
212 | 27,
213 | 201,
214 | 98,
215 | 162,
216 | 9,
217 | 154,
218 | 123,
219 | 31,
220 | 4,
221 | 100,
222 | 184,
223 | 187,
224 | 175,
225 | 225,
226 | 83,
227 | 95,
228 | 35,
229 | 1,
230 | 199,
231 | 44,
232 | 44,
233 | 179,
234 | 83,
235 | 91,
236 | 23,
237 | 45,
238 | 163,
239 | 11,
240 | 2,
241 | 104,
242 | 106,
243 | 176,
244 | 57,
245 | 61,
246 | 52,
247 | 134,
248 | 20,
249 | 241,
250 | 87,
251 | 251,
252 | 219
253 | ];
254 |
255 | uint8[] public pubkey4 = [
256 | 32,
257 | 184,
258 | 113,
259 | 243,
260 | 206,
261 | 208,
262 | 41,
263 | 225,
264 | 68,
265 | 114,
266 | 236,
267 | 78,
268 | 188,
269 | 60,
270 | 4,
271 | 72,
272 | 22,
273 | 73,
274 | 66,
275 | 177,
276 | 35,
277 | 170,
278 | 106,
279 | 249,
280 | 26,
281 | 51,
282 | 134,
283 | 193,
284 | 196,
285 | 3,
286 | 224,
287 | 235,
288 | 211,
289 | 180,
290 | 165,
291 | 117,
292 | 42,
293 | 43,
294 | 108,
295 | 73,
296 | 229,
297 | 116,
298 | 97,
299 | 158,
300 | 106,
301 | 160,
302 | 84,
303 | 158,
304 | 185,
305 | 204,
306 | 208,
307 | 54,
308 | 185,
309 | 187,
310 | 197,
311 | 7,
312 | 225,
313 | 247,
314 | 249,
315 | 113,
316 | 42,
317 | 35,
318 | 96,
319 | 146
320 | ];
321 |
322 | uint8[] public pubkey5 = [
323 | 191,
324 | 110,
325 | 230,
326 | 74,
327 | 141,
328 | 47,
329 | 220,
330 | 85,
331 | 30,
332 | 200,
333 | 187,
334 | 158,
335 | 248,
336 | 98,
337 | 239,
338 | 107,
339 | 75,
340 | 203,
341 | 24,
342 | 5,
343 | 205,
344 | 197,
345 | 32,
346 | 195,
347 | 170,
348 | 88,
349 | 102,
350 | 192,
351 | 87,
352 | 95,
353 | 211,
354 | 181,
355 | 20,
356 | 197,
357 | 86,
358 | 44,
359 | 60,
360 | 170,
361 | 231,
362 | 174,
363 | 197,
364 | 205,
365 | 111,
366 | 20,
367 | 75,
368 | 87,
369 | 19,
370 | 92,
371 | 117,
372 | 182,
373 | 246,
374 | 206,
375 | 160,
376 | 89,
377 | 195,
378 | 208,
379 | 141,
380 | 31,
381 | 57,
382 | 169,
383 | 194,
384 | 39,
385 | 33,
386 | 157
387 | ];
388 |
389 | // function getPubkey(uint _num) internal view returns(uint8[] memory) {
390 | // if
391 | // }
392 | }
393 |
--------------------------------------------------------------------------------
/test/Account.t.sol:
--------------------------------------------------------------------------------
1 | pragma solidity 0.8.12;
2 |
3 | import "../src/verifier/UltraVerifier.sol";
4 | import {ZkECDSAA} from "../src/erc4337/Account.sol";
5 | import "./utils/BytesLib.sol";
6 | import "./utils/NoirHelper.sol";
7 | import "./utils/pubkey/address.sol";
8 |
9 | import {EntryPoint} from "account-abstraction/core/EntryPoint.sol";
10 | import {UserOperation} from "account-abstraction/interfaces/UserOperation.sol";
11 | import {MockSetter} from "./mocks/MockSetter.sol";
12 | import {getUserOperation} from "./utils/userOp/Fixtures.sol";
13 |
14 | // writing to prover.toml fails if you run multiple test function with proof generation at the same time.
15 | // so you should specify the test function and run each seperately.
16 | // for instance forge test --contracts zkECDSAATest --match-test test_set_value -vv
17 |
18 | contract zkECDSAATest is NoirHelper, Addresses {
19 | using BytesLib for bytes;
20 |
21 | UltraVerifier public verifier;
22 | ZkECDSAA public zkECDSAA;
23 | MockSetter public mockSetter;
24 | EntryPoint public entryPoint;
25 |
26 | uint256 chainId = block.chainid;
27 |
28 | function setUp() public {
29 | entryPoint = new EntryPoint();
30 | verifier = new UltraVerifier();
31 | mockSetter = new MockSetter();
32 | zkECDSAA = new ZkECDSAA(address(entryPoint));
33 |
34 | bytes32[] memory guardians = new bytes32[](3);
35 | guardians[0] = hashedAddr[0];
36 | guardians[1] = hashedAddr[1];
37 | guardians[2] = hashedAddr[2];
38 |
39 | zkECDSAA.initialize(
40 | hashedAddr[0],
41 | address(verifier),
42 | guardians,
43 | uint8(2),
44 | 864000,
45 | hashedAddr[4]
46 | );
47 |
48 | vm.deal(address(zkECDSAA), 5 ether);
49 | }
50 |
51 | // basic transaction
52 | function test_set_value() public {
53 | bytes memory callData = abi.encodeWithSignature("setValue(uint256)", 1);
54 |
55 | string memory proof_name = "set";
56 |
57 | uint res = prove_and_verify(
58 | callData,
59 | string.concat("circuits/zkecdsa/proofs/", proof_name, ".proof"),
60 | proof_name,
61 | 0,
62 | 0,
63 | pubkey1
64 | );
65 | assertEq(res, 0);
66 |
67 | vm.prank(address(entryPoint));
68 | zkECDSAA.execute(address(mockSetter), 0, callData);
69 |
70 | uint value = mockSetter.value();
71 | assertEq(value, 1);
72 | }
73 |
74 | // function test_approve_recovery_() public {
75 | // vm.prank(addresses[3]);
76 | // zkECDSAA.proposeRecovery(hashedAddr[3], block.timestamp + 864000);
77 |
78 | // vm.prank(addresses[3]);
79 | // zkECDSAA.approveRecovery(hashedAddr[3], hashedAddr[1]);
80 |
81 | // uint appCount = zkECDSAA.getApprovalCount(hashedAddr[3]);
82 | // assertEq(appCount, 1);
83 | // }
84 |
85 | function test_approve_recovery() public {
86 | bytes32 owner_ = zkECDSAA.owner();
87 | assertEq(owner_, hashedAddr[0]);
88 |
89 | console.log("The initial owner: ");
90 | console.logBytes32(owner_);
91 |
92 | vm.prank(addresses[3]);
93 | zkECDSAA.proposeRecovery(hashedAddr[3], block.timestamp + 864000);
94 |
95 | uint _appCount = zkECDSAA.getApprovalCount(hashedAddr[3]);
96 | assertEq(_appCount, 0);
97 |
98 | console.log("");
99 | console.log("New Recovery Proposed:");
100 | console.log("appoval count: ", _appCount);
101 |
102 | bytes memory callData = abi.encodeWithSelector(
103 | zkECDSAA.approveRecovery.selector,
104 | hashedAddr[3],
105 | hashedAddr[1]
106 | );
107 |
108 | string memory proof_name = "app_r";
109 |
110 | //console.logBytes(callData);
111 |
112 | // validation
113 | uint res = prove_and_verify(
114 | callData,
115 | string.concat("circuits/zkecdsa/proofs/", proof_name, ".proof"),
116 | proof_name,
117 | 1, // pk
118 | 1, // hashedaddr
119 | pubkey2
120 | );
121 |
122 | assertEq(res, 0);
123 |
124 | // execute
125 | vm.prank(address(entryPoint));
126 | zkECDSAA.execute(address(zkECDSAA), 0, callData);
127 |
128 | uint appCount = zkECDSAA.getApprovalCount(hashedAddr[3]);
129 | assertEq(appCount, 1);
130 |
131 | console.log("");
132 | console.log("The Recovery Proposal is approved by the first Guardian:");
133 | console.log("appoval count: ", appCount);
134 |
135 | bool voted = zkECDSAA.getVoted(hashedAddr[3], hashedAddr[1]);
136 | assertEq(voted, true);
137 |
138 | // second approval
139 |
140 | bytes memory callData_ = abi.encodeWithSelector(
141 | zkECDSAA.approveRecovery.selector,
142 | hashedAddr[3],
143 | hashedAddr[2]
144 | );
145 |
146 | uint res_ = prove_and_verify(
147 | callData_,
148 | string.concat("circuits/zkecdsa/proofs/", proof_name, ".proof"),
149 | proof_name,
150 | 2, // pk
151 | 2, // hashedaddr
152 | pubkey3
153 | );
154 |
155 | assertEq(res_, 0);
156 |
157 | // execute
158 | vm.prank(address(entryPoint));
159 | zkECDSAA.execute(address(zkECDSAA), 0, callData_);
160 |
161 | appCount = zkECDSAA.getApprovalCount(hashedAddr[3]);
162 | assertEq(appCount, 2);
163 |
164 | console.log("");
165 | console.log(
166 | "The Recovery Proposal is approved by the second Guardian:"
167 | );
168 | console.log("appoval count: ", appCount);
169 |
170 | bool voted_ = zkECDSAA.getVoted(hashedAddr[3], hashedAddr[2]);
171 | assertEq(voted_, true);
172 |
173 | bytes32 owner = zkECDSAA.owner();
174 | assertEq(owner, hashedAddr[3]);
175 |
176 | console.log("");
177 | console.log("The new onwer has been set:");
178 | console.logBytes32(owner);
179 | }
180 |
181 | function test_inheritance() public {
182 | // vm.prank(addresses[4]);
183 | // zkECDSAA.proposeInheritance(hashedAddr[4]);
184 |
185 | bytes memory callData = abi.encodeWithSelector(
186 | zkECDSAA.proposeInheritance.selector,
187 | hashedAddr[4]
188 | );
189 |
190 | // console.logBytes(callData);
191 |
192 | string memory proof_name = "prop_i";
193 |
194 | uint res = prove_and_verify(
195 | callData,
196 | string.concat("circuits/zkecdsa/proofs/", proof_name, ".proof"),
197 | proof_name,
198 | 4, // pk
199 | 4, // hashedaddr
200 | pubkey5
201 | );
202 |
203 | assertEq(res, 0);
204 |
205 | // execute
206 | vm.prank(address(entryPoint));
207 | zkECDSAA.execute(address(zkECDSAA), 0, callData);
208 |
209 | bytes32 benf = zkECDSAA.getBeneficiary(1);
210 | assertEq(benf, hashedAddr[4]);
211 |
212 | vm.warp(block.timestamp + 865000);
213 |
214 | bytes memory callData_ = abi.encodeWithSelector(
215 | zkECDSAA.executeInheritance.selector,
216 | hashedAddr[4],
217 | 1
218 | );
219 |
220 | proof_name = "exec_i";
221 |
222 | uint res_ = prove_and_verify(
223 | callData_,
224 | string.concat("circuits/zkecdsa/proofs/", proof_name, ".proof"),
225 | proof_name,
226 | 4, // pk
227 | 4, // hashedaddr
228 | pubkey5
229 | );
230 |
231 | assertEq(res_, 0);
232 |
233 | // execute
234 | vm.prank(address(entryPoint));
235 | zkECDSAA.execute(address(zkECDSAA), 0, callData_);
236 |
237 | bytes32 owner = zkECDSAA.owner();
238 | assertEq(owner, hashedAddr[4]);
239 | }
240 |
241 | function prove_and_verify(
242 | bytes memory _calldata,
243 | string memory _path,
244 | string memory _proof_name,
245 | uint pk_num,
246 | uint hashed_addr_num,
247 | uint8[] memory pubkey
248 | ) internal returns (uint) {
249 | (UserOperation memory userOp, bytes32 userOpHash) = _getUserOperation(
250 | _calldata
251 | );
252 |
253 | bytes memory signature = _getSignature(private_key[pk_num], userOpHash);
254 |
255 | this.withInput(
256 | "hashedAddr",
257 | hashedAddr[hashed_addr_num],
258 | "pub_key",
259 | pubkey,
260 | "signature",
261 | BytesLib.bytesToUint8Array(signature),
262 | "message_hash",
263 | BytesLib.bytes32ToUint8Array(userOpHash)
264 | );
265 |
266 | bytes memory proof = generateProof(_path, _proof_name);
267 | //bytes memory proof = vm.parseBytes(vm.readFile(_path));
268 | userOp.signature = abi.encode(hashedAddr[hashed_addr_num], proof);
269 |
270 | vm.prank(address(entryPoint));
271 | uint256 ValidatinRes = zkECDSAA.validateUserOp(userOp, userOpHash, 0);
272 | assertEq(ValidatinRes, 0);
273 |
274 | return 0;
275 | }
276 |
277 | function _getSignature(
278 | uint _privatekey,
279 | bytes32 _userOpHash
280 | ) internal view returns (bytes memory) {
281 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(
282 | _privatekey, // here
283 | _userOpHash
284 | );
285 |
286 | bytes memory signature = BytesLib.sliceStartEnd(
287 | abi.encodePacked(r, s, v),
288 | 0,
289 | 64
290 | );
291 | //console.logBytes(signature);
292 |
293 | return signature;
294 | }
295 |
296 | function _getUserOperation(
297 | bytes memory _calldata
298 | ) internal view returns (UserOperation memory userOp, bytes32 userOpHash) {
299 | return
300 | getUserOperation(
301 | address(zkECDSAA),
302 | zkECDSAA.getNonce(),
303 | _calldata,
304 | address(entryPoint),
305 | uint8(chainId),
306 | vm
307 | );
308 | }
309 | }
310 |
--------------------------------------------------------------------------------
/src/erc4337/external/EntryPoint.sol:
--------------------------------------------------------------------------------
1 | /**
2 | ** Account-Abstraction (EIP-4337) singleton EntryPoint implementation.
3 | ** Only one instance required on each chain.
4 | **/
5 | // SPDX-License-Identifier: GPL-3.0
6 | pragma solidity ^0.8.12;
7 |
8 | /* solhint-disable avoid-low-level-calls */
9 | /* solhint-disable no-inline-assembly */
10 |
11 | import "account-abstraction/interfaces/IAccount.sol";
12 | import "account-abstraction/interfaces/IPaymaster.sol";
13 | import "account-abstraction/interfaces/IEntryPoint.sol";
14 |
15 | import "account-abstraction/utils/Exec.sol";
16 | import "account-abstraction/core/StakeManager.sol";
17 | import "account-abstraction/core/SenderCreator.sol";
18 | import "account-abstraction/core/Helpers.sol";
19 | import "account-abstraction/core/NonceManager.sol";
20 | import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
21 |
22 | contract EntryPoint is
23 | IEntryPoint,
24 | StakeManager,
25 | NonceManager,
26 | ReentrancyGuard
27 | {
28 | using UserOperationLib for UserOperation;
29 |
30 | SenderCreator private immutable senderCreator = new SenderCreator();
31 |
32 | // internal value used during simulation: need to query aggregator.
33 | address private constant SIMULATE_FIND_AGGREGATOR = address(1);
34 |
35 | // marker for inner call revert on out of gas
36 | bytes32 private constant INNER_OUT_OF_GAS = hex"deaddead";
37 |
38 | uint256 private constant REVERT_REASON_MAX_LEN = 2048;
39 |
40 | /**
41 | * for simulation purposes, validateUserOp (and validatePaymasterUserOp) must return this value
42 | * in case of signature failure, instead of revert.
43 | */
44 | uint256 public constant SIG_VALIDATION_FAILED = 1;
45 |
46 | /**
47 | * compensate the caller's beneficiary address with the collected fees of all UserOperations.
48 | * @param beneficiary the address to receive the fees
49 | * @param amount amount to transfer.
50 | */
51 | function _compensate(address payable beneficiary, uint256 amount) internal {
52 | require(beneficiary != address(0), "AA90 invalid beneficiary");
53 | (bool success, ) = beneficiary.call{value: amount}("");
54 | require(success, "AA91 failed send to beneficiary");
55 | }
56 |
57 | /**
58 | * execute a user op
59 | * @param opIndex index into the opInfo array
60 | * @param userOp the userOp to execute
61 | * @param opInfo the opInfo filled by validatePrepayment for this userOp.
62 | * @return collected the total amount this userOp paid.
63 | */
64 | function _executeUserOp(
65 | uint256 opIndex,
66 | UserOperation calldata userOp,
67 | UserOpInfo memory opInfo
68 | ) private returns (uint256 collected) {
69 | uint256 preGas = gasleft();
70 | bytes memory context = getMemoryBytesFromOffset(opInfo.contextOffset);
71 |
72 | try this.innerHandleOp(userOp.callData, opInfo, context) returns (
73 | uint256 _actualGasCost
74 | ) {
75 | collected = _actualGasCost;
76 | } catch {
77 | bytes32 innerRevertCode;
78 | assembly {
79 | returndatacopy(0, 0, 32)
80 | innerRevertCode := mload(0)
81 | }
82 | // handleOps was called with gas limit too low. abort entire bundle.
83 | if (innerRevertCode == INNER_OUT_OF_GAS) {
84 | //report paymaster, since if it is not deliberately caused by the bundler,
85 | // it must be a revert caused by paymaster.
86 | revert FailedOp(opIndex, "AA95 out of gas");
87 | }
88 |
89 | uint256 actualGas = preGas - gasleft() + opInfo.preOpGas;
90 | collected = _handlePostOp(
91 | opIndex,
92 | IPaymaster.PostOpMode.postOpReverted,
93 | opInfo,
94 | context,
95 | actualGas
96 | );
97 | }
98 | }
99 |
100 | /**
101 | * Execute a batch of UserOperations.
102 | * no signature aggregator is used.
103 | * if any account requires an aggregator (that is, it returned an aggregator when
104 | * performing simulateValidation), then handleAggregatedOps() must be used instead.
105 | * @param ops the operations to execute
106 | * @param beneficiary the address to receive the fees
107 | */
108 | function handleOps(
109 | UserOperation[] calldata ops,
110 | address payable beneficiary
111 | ) public nonReentrant {
112 | uint256 opslen = ops.length;
113 | UserOpInfo[] memory opInfos = new UserOpInfo[](opslen);
114 |
115 | unchecked {
116 | for (uint256 i = 0; i < opslen; i++) {
117 | UserOpInfo memory opInfo = opInfos[i];
118 | (
119 | uint256 validationData,
120 | uint256 pmValidationData
121 | ) = _validatePrepayment(i, ops[i], opInfo);
122 | _validateAccountAndPaymasterValidationData(
123 | i,
124 | validationData,
125 | pmValidationData,
126 | address(0)
127 | );
128 | }
129 |
130 | uint256 collected = 0;
131 | emit BeforeExecution();
132 |
133 | for (uint256 i = 0; i < opslen; i++) {
134 | collected += _executeUserOp(i, ops[i], opInfos[i]);
135 | }
136 |
137 | _compensate(beneficiary, collected);
138 | } //unchecked
139 | }
140 |
141 | /**
142 | * Execute a batch of UserOperation with Aggregators
143 | * @param opsPerAggregator the operations to execute, grouped by aggregator (or address(0) for no-aggregator accounts)
144 | * @param beneficiary the address to receive the fees
145 | */
146 | function handleAggregatedOps(
147 | UserOpsPerAggregator[] calldata opsPerAggregator,
148 | address payable beneficiary
149 | ) public nonReentrant {
150 | uint256 opasLen = opsPerAggregator.length;
151 | uint256 totalOps = 0;
152 | for (uint256 i = 0; i < opasLen; i++) {
153 | UserOpsPerAggregator calldata opa = opsPerAggregator[i];
154 | UserOperation[] calldata ops = opa.userOps;
155 | IAggregator aggregator = opa.aggregator;
156 |
157 | //address(1) is special marker of "signature error"
158 | require(
159 | address(aggregator) != address(1),
160 | "AA96 invalid aggregator"
161 | );
162 |
163 | if (address(aggregator) != address(0)) {
164 | // solhint-disable-next-line no-empty-blocks
165 | try aggregator.validateSignatures(ops, opa.signature) {} catch {
166 | revert SignatureValidationFailed(address(aggregator));
167 | }
168 | }
169 |
170 | totalOps += ops.length;
171 | }
172 |
173 | UserOpInfo[] memory opInfos = new UserOpInfo[](totalOps);
174 |
175 | emit BeforeExecution();
176 |
177 | uint256 opIndex = 0;
178 | for (uint256 a = 0; a < opasLen; a++) {
179 | UserOpsPerAggregator calldata opa = opsPerAggregator[a];
180 | UserOperation[] calldata ops = opa.userOps;
181 | IAggregator aggregator = opa.aggregator;
182 |
183 | uint256 opslen = ops.length;
184 | for (uint256 i = 0; i < opslen; i++) {
185 | UserOpInfo memory opInfo = opInfos[opIndex];
186 | (
187 | uint256 validationData,
188 | uint256 paymasterValidationData
189 | ) = _validatePrepayment(opIndex, ops[i], opInfo);
190 | _validateAccountAndPaymasterValidationData(
191 | i,
192 | validationData,
193 | paymasterValidationData,
194 | address(aggregator)
195 | );
196 | opIndex++;
197 | }
198 | }
199 |
200 | uint256 collected = 0;
201 | opIndex = 0;
202 | for (uint256 a = 0; a < opasLen; a++) {
203 | UserOpsPerAggregator calldata opa = opsPerAggregator[a];
204 | emit SignatureAggregatorChanged(address(opa.aggregator));
205 | UserOperation[] calldata ops = opa.userOps;
206 | uint256 opslen = ops.length;
207 |
208 | for (uint256 i = 0; i < opslen; i++) {
209 | collected += _executeUserOp(opIndex, ops[i], opInfos[opIndex]);
210 | opIndex++;
211 | }
212 | }
213 | emit SignatureAggregatorChanged(address(0));
214 |
215 | _compensate(beneficiary, collected);
216 | }
217 |
218 | /// @inheritdoc IEntryPoint
219 | function simulateHandleOp(
220 | UserOperation calldata op,
221 | address target,
222 | bytes calldata targetCallData
223 | ) external override {
224 | UserOpInfo memory opInfo;
225 | _simulationOnlyValidations(op);
226 | (
227 | uint256 validationData,
228 | uint256 paymasterValidationData
229 | ) = _validatePrepayment(0, op, opInfo);
230 | ValidationData memory data = _intersectTimeRange(
231 | validationData,
232 | paymasterValidationData
233 | );
234 |
235 | numberMarker();
236 | uint256 paid = _executeUserOp(0, op, opInfo);
237 | numberMarker();
238 | bool targetSuccess;
239 | bytes memory targetResult;
240 | if (target != address(0)) {
241 | (targetSuccess, targetResult) = target.call(targetCallData);
242 | }
243 | revert ExecutionResult(
244 | opInfo.preOpGas,
245 | paid,
246 | data.validAfter,
247 | data.validUntil,
248 | targetSuccess,
249 | targetResult
250 | );
251 | }
252 |
253 | // A memory copy of UserOp static fields only.
254 | // Excluding: callData, initCode and signature. Replacing paymasterAndData with paymaster.
255 | struct MemoryUserOp {
256 | address sender;
257 | uint256 nonce;
258 | uint256 callGasLimit;
259 | uint256 verificationGasLimit;
260 | uint256 preVerificationGas;
261 | address paymaster;
262 | uint256 maxFeePerGas;
263 | uint256 maxPriorityFeePerGas;
264 | }
265 |
266 | struct UserOpInfo {
267 | MemoryUserOp mUserOp;
268 | bytes32 userOpHash;
269 | uint256 prefund;
270 | uint256 contextOffset;
271 | uint256 preOpGas;
272 | }
273 |
274 | /**
275 | * inner function to handle a UserOperation.
276 | * Must be declared "external" to open a call context, but it can only be called by handleOps.
277 | */
278 | function innerHandleOp(
279 | bytes memory callData,
280 | UserOpInfo memory opInfo,
281 | bytes calldata context
282 | ) external returns (uint256 actualGasCost) {
283 | uint256 preGas = gasleft();
284 | require(msg.sender == address(this), "AA92 internal call only");
285 | MemoryUserOp memory mUserOp = opInfo.mUserOp;
286 |
287 | uint callGasLimit = mUserOp.callGasLimit;
288 | unchecked {
289 | // handleOps was called with gas limit too low. abort entire bundle.
290 | if (
291 | gasleft() < callGasLimit + mUserOp.verificationGasLimit + 5000
292 | ) {
293 | assembly {
294 | mstore(0, INNER_OUT_OF_GAS)
295 | revert(0, 32)
296 | }
297 | }
298 | }
299 |
300 | IPaymaster.PostOpMode mode = IPaymaster.PostOpMode.opSucceeded;
301 | if (callData.length > 0) {
302 | bool success = Exec.call(mUserOp.sender, 0, callData, callGasLimit);
303 | if (!success) {
304 | bytes memory result = Exec.getReturnData(REVERT_REASON_MAX_LEN);
305 | if (result.length > 0) {
306 | emit UserOperationRevertReason(
307 | opInfo.userOpHash,
308 | mUserOp.sender,
309 | mUserOp.nonce,
310 | result
311 | );
312 | }
313 | mode = IPaymaster.PostOpMode.opReverted;
314 | }
315 | }
316 |
317 | unchecked {
318 | uint256 actualGas = preGas - gasleft() + opInfo.preOpGas;
319 | //note: opIndex is ignored (relevant only if mode==postOpReverted, which is only possible outside of innerHandleOp)
320 | return _handlePostOp(0, mode, opInfo, context, actualGas);
321 | }
322 | }
323 |
324 | /**
325 | * generate a request Id - unique identifier for this request.
326 | * the request ID is a hash over the content of the userOp (except the signature), the entrypoint and the chainid.
327 | */
328 | function getUserOpHash(
329 | UserOperation calldata userOp
330 | ) public view returns (bytes32) {
331 | return
332 | keccak256(abi.encode(userOp.hash(), address(this), block.chainid));
333 | }
334 |
335 | /**
336 | * copy general fields from userOp into the memory opInfo structure.
337 | */
338 | function _copyUserOpToMemory(
339 | UserOperation calldata userOp,
340 | MemoryUserOp memory mUserOp
341 | ) internal pure {
342 | mUserOp.sender = userOp.sender;
343 | mUserOp.nonce = userOp.nonce;
344 | mUserOp.callGasLimit = userOp.callGasLimit;
345 | mUserOp.verificationGasLimit = userOp.verificationGasLimit;
346 | mUserOp.preVerificationGas = userOp.preVerificationGas;
347 | mUserOp.maxFeePerGas = userOp.maxFeePerGas;
348 | mUserOp.maxPriorityFeePerGas = userOp.maxPriorityFeePerGas;
349 | bytes calldata paymasterAndData = userOp.paymasterAndData;
350 | if (paymasterAndData.length > 0) {
351 | require(
352 | paymasterAndData.length >= 20,
353 | "AA93 invalid paymasterAndData"
354 | );
355 | mUserOp.paymaster = address(bytes20(paymasterAndData[:20]));
356 | } else {
357 | mUserOp.paymaster = address(0);
358 | }
359 | }
360 |
361 | /**
362 | * Simulate a call to account.validateUserOp and paymaster.validatePaymasterUserOp.
363 | * @dev this method always revert. Successful result is ValidationResult error. other errors are failures.
364 | * @dev The node must also verify it doesn't use banned opcodes, and that it doesn't reference storage outside the account's data.
365 | * @param userOp the user operation to validate.
366 | */
367 | function simulateValidation(UserOperation calldata userOp) external {
368 | UserOpInfo memory outOpInfo;
369 |
370 | _simulationOnlyValidations(userOp);
371 | (
372 | uint256 validationData,
373 | uint256 paymasterValidationData
374 | ) = _validatePrepayment(0, userOp, outOpInfo);
375 | StakeInfo memory paymasterInfo = _getStakeInfo(
376 | outOpInfo.mUserOp.paymaster
377 | );
378 | StakeInfo memory senderInfo = _getStakeInfo(outOpInfo.mUserOp.sender);
379 | StakeInfo memory factoryInfo;
380 | {
381 | bytes calldata initCode = userOp.initCode;
382 | address factory = initCode.length >= 20
383 | ? address(bytes20(initCode[0:20]))
384 | : address(0);
385 | factoryInfo = _getStakeInfo(factory);
386 | }
387 |
388 | ValidationData memory data = _intersectTimeRange(
389 | validationData,
390 | paymasterValidationData
391 | );
392 | address aggregator = data.aggregator;
393 | bool sigFailed = aggregator == address(1);
394 | ReturnInfo memory returnInfo = ReturnInfo(
395 | outOpInfo.preOpGas,
396 | outOpInfo.prefund,
397 | sigFailed,
398 | data.validAfter,
399 | data.validUntil,
400 | getMemoryBytesFromOffset(outOpInfo.contextOffset)
401 | );
402 |
403 | if (aggregator != address(0) && aggregator != address(1)) {
404 | AggregatorStakeInfo memory aggregatorInfo = AggregatorStakeInfo(
405 | aggregator,
406 | _getStakeInfo(aggregator)
407 | );
408 | revert ValidationResultWithAggregation(
409 | returnInfo,
410 | senderInfo,
411 | factoryInfo,
412 | paymasterInfo,
413 | aggregatorInfo
414 | );
415 | }
416 | revert ValidationResult(
417 | returnInfo,
418 | senderInfo,
419 | factoryInfo,
420 | paymasterInfo
421 | );
422 | }
423 |
424 | function _getRequiredPrefund(
425 | MemoryUserOp memory mUserOp
426 | ) internal pure returns (uint256 requiredPrefund) {
427 | unchecked {
428 | //when using a Paymaster, the verificationGasLimit is used also to as a limit for the postOp call.
429 | // our security model might call postOp eventually twice
430 | uint256 mul = mUserOp.paymaster != address(0) ? 3 : 1;
431 | uint256 requiredGas = mUserOp.callGasLimit +
432 | mUserOp.verificationGasLimit *
433 | mul +
434 | mUserOp.preVerificationGas;
435 |
436 | requiredPrefund = requiredGas * mUserOp.maxFeePerGas;
437 | }
438 | }
439 |
440 | // create the sender's contract if needed.
441 | function _createSenderIfNeeded(
442 | uint256 opIndex,
443 | UserOpInfo memory opInfo,
444 | bytes calldata initCode
445 | ) internal {
446 | if (initCode.length != 0) {
447 | address sender = opInfo.mUserOp.sender;
448 | if (sender.code.length != 0)
449 | revert FailedOp(opIndex, "AA10 sender already constructed");
450 | address sender1 = senderCreator.createSender{
451 | gas: opInfo.mUserOp.verificationGasLimit
452 | }(initCode);
453 | if (sender1 == address(0))
454 | revert FailedOp(opIndex, "AA13 initCode failed or OOG");
455 | if (sender1 != sender)
456 | revert FailedOp(opIndex, "AA14 initCode must return sender");
457 | if (sender1.code.length == 0)
458 | revert FailedOp(opIndex, "AA15 initCode must create sender");
459 | address factory = address(bytes20(initCode[0:20]));
460 | emit AccountDeployed(
461 | opInfo.userOpHash,
462 | sender,
463 | factory,
464 | opInfo.mUserOp.paymaster
465 | );
466 | }
467 | }
468 |
469 | /**
470 | * Get counterfactual sender address.
471 | * Calculate the sender contract address that will be generated by the initCode and salt in the UserOperation.
472 | * this method always revert, and returns the address in SenderAddressResult error
473 | * @param initCode the constructor code to be passed into the UserOperation.
474 | */
475 | function getSenderAddress(bytes calldata initCode) public {
476 | address sender = senderCreator.createSender(initCode);
477 | revert SenderAddressResult(sender);
478 | }
479 |
480 | function _simulationOnlyValidations(
481 | UserOperation calldata userOp
482 | ) internal view {
483 | // solhint-disable-next-line no-empty-blocks
484 | try
485 | this._validateSenderAndPaymaster(
486 | userOp.initCode,
487 | userOp.sender,
488 | userOp.paymasterAndData
489 | )
490 | {} catch Error(string memory revertReason) {
491 | if (bytes(revertReason).length != 0) {
492 | revert FailedOp(0, revertReason);
493 | }
494 | }
495 | }
496 |
497 | /**
498 | * Called only during simulation.
499 | * This function always reverts to prevent warm/cold storage differentiation in simulation vs execution.
500 | */
501 | function _validateSenderAndPaymaster(
502 | bytes calldata initCode,
503 | address sender,
504 | bytes calldata paymasterAndData
505 | ) external view {
506 | if (initCode.length == 0 && sender.code.length == 0) {
507 | // it would revert anyway. but give a meaningful message
508 | revert("AA20 account not deployed");
509 | }
510 | if (paymasterAndData.length >= 20) {
511 | address paymaster = address(bytes20(paymasterAndData[0:20]));
512 | if (paymaster.code.length == 0) {
513 | // it would revert anyway. but give a meaningful message
514 | revert("AA30 paymaster not deployed");
515 | }
516 | }
517 | // always revert
518 | revert("");
519 | }
520 |
521 | /**
522 | * call account.validateUserOp.
523 | * revert (with FailedOp) in case validateUserOp reverts, or account didn't send required prefund.
524 | * decrement account's deposit if needed
525 | */
526 | function _validateAccountPrepayment(
527 | uint256 opIndex,
528 | UserOperation calldata op,
529 | UserOpInfo memory opInfo,
530 | uint256 requiredPrefund
531 | )
532 | internal
533 | returns (
534 | uint256 gasUsedByValidateAccountPrepayment,
535 | uint256 validationData
536 | )
537 | {
538 | unchecked {
539 | uint256 preGas = gasleft();
540 | MemoryUserOp memory mUserOp = opInfo.mUserOp;
541 | address sender = mUserOp.sender;
542 | _createSenderIfNeeded(opIndex, opInfo, op.initCode);
543 | address paymaster = mUserOp.paymaster;
544 | numberMarker();
545 | uint256 missingAccountFunds = 0;
546 | if (paymaster == address(0)) {
547 | uint256 bal = balanceOf(sender);
548 | missingAccountFunds = bal > requiredPrefund
549 | ? 0
550 | : requiredPrefund - bal;
551 | }
552 | try
553 | IAccount(sender).validateUserOp{
554 | gas: mUserOp.verificationGasLimit
555 | }(op, opInfo.userOpHash, missingAccountFunds)
556 | returns (uint256 _validationData) {
557 | validationData = _validationData;
558 | } catch Error(string memory revertReason) {
559 | revert FailedOp(
560 | opIndex,
561 | string.concat("AA23 reverted: ", revertReason)
562 | );
563 | } catch {
564 | revert FailedOp(opIndex, "AA23 reverted (or OOG)");
565 | }
566 | if (paymaster == address(0)) {
567 | DepositInfo storage senderInfo = deposits[sender];
568 | uint256 deposit = senderInfo.deposit;
569 | if (requiredPrefund > deposit) {
570 | revert FailedOp(opIndex, "AA21 didn't pay prefund");
571 | }
572 | senderInfo.deposit = uint112(deposit - requiredPrefund);
573 | }
574 | gasUsedByValidateAccountPrepayment = preGas - gasleft();
575 | }
576 | }
577 |
578 | /**
579 | * In case the request has a paymaster:
580 | * Validate paymaster has enough deposit.
581 | * Call paymaster.validatePaymasterUserOp.
582 | * Revert with proper FailedOp in case paymaster reverts.
583 | * Decrement paymaster's deposit
584 | */
585 | function _validatePaymasterPrepayment(
586 | uint256 opIndex,
587 | UserOperation calldata op,
588 | UserOpInfo memory opInfo,
589 | uint256 requiredPreFund,
590 | uint256 gasUsedByValidateAccountPrepayment
591 | ) internal returns (bytes memory context, uint256 validationData) {
592 | unchecked {
593 | MemoryUserOp memory mUserOp = opInfo.mUserOp;
594 | uint256 verificationGasLimit = mUserOp.verificationGasLimit;
595 | require(
596 | verificationGasLimit > gasUsedByValidateAccountPrepayment,
597 | "AA41 too little verificationGas"
598 | );
599 | uint256 gas = verificationGasLimit -
600 | gasUsedByValidateAccountPrepayment;
601 |
602 | address paymaster = mUserOp.paymaster;
603 | DepositInfo storage paymasterInfo = deposits[paymaster];
604 | uint256 deposit = paymasterInfo.deposit;
605 | if (deposit < requiredPreFund) {
606 | revert FailedOp(opIndex, "AA31 paymaster deposit too low");
607 | }
608 | paymasterInfo.deposit = uint112(deposit - requiredPreFund);
609 | try
610 | IPaymaster(paymaster).validatePaymasterUserOp{gas: gas}(
611 | op,
612 | opInfo.userOpHash,
613 | requiredPreFund
614 | )
615 | returns (bytes memory _context, uint256 _validationData) {
616 | context = _context;
617 | validationData = _validationData;
618 | } catch Error(string memory revertReason) {
619 | revert FailedOp(
620 | opIndex,
621 | string.concat("AA33 reverted: ", revertReason)
622 | );
623 | } catch {
624 | revert FailedOp(opIndex, "AA33 reverted (or OOG)");
625 | }
626 | }
627 | }
628 |
629 | /**
630 | * revert if either account validationData or paymaster validationData is expired
631 | */
632 | function _validateAccountAndPaymasterValidationData(
633 | uint256 opIndex,
634 | uint256 validationData,
635 | uint256 paymasterValidationData,
636 | address expectedAggregator
637 | ) internal view {
638 | (address aggregator, bool outOfTimeRange) = _getValidationData(
639 | validationData
640 | );
641 | if (expectedAggregator != aggregator) {
642 | revert FailedOp(opIndex, "AA24 signature error");
643 | }
644 | if (outOfTimeRange) {
645 | revert FailedOp(opIndex, "AA22 expired or not due");
646 | }
647 | //pmAggregator is not a real signature aggregator: we don't have logic to handle it as address.
648 | // non-zero address means that the paymaster fails due to some signature check (which is ok only during estimation)
649 | address pmAggregator;
650 | (pmAggregator, outOfTimeRange) = _getValidationData(
651 | paymasterValidationData
652 | );
653 | if (pmAggregator != address(0)) {
654 | revert FailedOp(opIndex, "AA34 signature error");
655 | }
656 | if (outOfTimeRange) {
657 | revert FailedOp(opIndex, "AA32 paymaster expired or not due");
658 | }
659 | }
660 |
661 | function _getValidationData(
662 | uint256 validationData
663 | ) internal view returns (address aggregator, bool outOfTimeRange) {
664 | if (validationData == 0) {
665 | return (address(0), false);
666 | }
667 | ValidationData memory data = _parseValidationData(validationData);
668 | // solhint-disable-next-line not-rely-on-time
669 | outOfTimeRange =
670 | block.timestamp > data.validUntil ||
671 | block.timestamp < data.validAfter;
672 | aggregator = data.aggregator;
673 | }
674 |
675 | /**
676 | * validate account and paymaster (if defined).
677 | * also make sure total validation doesn't exceed verificationGasLimit
678 | * this method is called off-chain (simulateValidation()) and on-chain (from handleOps)
679 | * @param opIndex the index of this userOp into the "opInfos" array
680 | * @param userOp the userOp to validate
681 | */
682 | function _validatePrepayment(
683 | uint256 opIndex,
684 | UserOperation calldata userOp,
685 | UserOpInfo memory outOpInfo
686 | )
687 | private
688 | returns (uint256 validationData, uint256 paymasterValidationData)
689 | {
690 | uint256 preGas = gasleft();
691 | MemoryUserOp memory mUserOp = outOpInfo.mUserOp;
692 | _copyUserOpToMemory(userOp, mUserOp);
693 | outOpInfo.userOpHash = getUserOpHash(userOp);
694 |
695 | // validate all numeric values in userOp are well below 128 bit, so they can safely be added
696 | // and multiplied without causing overflow
697 | uint256 maxGasValues = mUserOp.preVerificationGas |
698 | mUserOp.verificationGasLimit |
699 | mUserOp.callGasLimit |
700 | userOp.maxFeePerGas |
701 | userOp.maxPriorityFeePerGas;
702 | require(maxGasValues <= type(uint120).max, "AA94 gas values overflow");
703 |
704 | uint256 gasUsedByValidateAccountPrepayment;
705 | uint256 requiredPreFund = _getRequiredPrefund(mUserOp);
706 | (
707 | gasUsedByValidateAccountPrepayment,
708 | validationData
709 | ) = _validateAccountPrepayment(
710 | opIndex,
711 | userOp,
712 | outOpInfo,
713 | requiredPreFund
714 | );
715 |
716 | if (!_validateAndUpdateNonce(mUserOp.sender, mUserOp.nonce)) {
717 | revert FailedOp(opIndex, "AA25 invalid account nonce");
718 | }
719 |
720 | //a "marker" where account opcode validation is done and paymaster opcode validation is about to start
721 | // (used only by off-chain simulateValidation)
722 | numberMarker();
723 |
724 | bytes memory context;
725 | if (mUserOp.paymaster != address(0)) {
726 | (context, paymasterValidationData) = _validatePaymasterPrepayment(
727 | opIndex,
728 | userOp,
729 | outOpInfo,
730 | requiredPreFund,
731 | gasUsedByValidateAccountPrepayment
732 | );
733 | }
734 | unchecked {
735 | uint256 gasUsed = preGas - gasleft();
736 |
737 | if (userOp.verificationGasLimit < gasUsed) {
738 | revert FailedOp(opIndex, "AA40 over verificationGasLimit");
739 | }
740 | outOpInfo.prefund = requiredPreFund;
741 | outOpInfo.contextOffset = getOffsetOfMemoryBytes(context);
742 | outOpInfo.preOpGas = preGas - gasleft() + userOp.preVerificationGas;
743 | }
744 | }
745 |
746 | /**
747 | * process post-operation.
748 | * called just after the callData is executed.
749 | * if a paymaster is defined and its validation returned a non-empty context, its postOp is called.
750 | * the excess amount is refunded to the account (or paymaster - if it was used in the request)
751 | * @param opIndex index in the batch
752 | * @param mode - whether is called from innerHandleOp, or outside (postOpReverted)
753 | * @param opInfo userOp fields and info collected during validation
754 | * @param context the context returned in validatePaymasterUserOp
755 | * @param actualGas the gas used so far by this user operation
756 | */
757 | function _handlePostOp(
758 | uint256 opIndex,
759 | IPaymaster.PostOpMode mode,
760 | UserOpInfo memory opInfo,
761 | bytes memory context,
762 | uint256 actualGas
763 | ) private returns (uint256 actualGasCost) {
764 | uint256 preGas = gasleft();
765 | unchecked {
766 | address refundAddress;
767 | MemoryUserOp memory mUserOp = opInfo.mUserOp;
768 | uint256 gasPrice = getUserOpGasPrice(mUserOp);
769 |
770 | address paymaster = mUserOp.paymaster;
771 | if (paymaster == address(0)) {
772 | refundAddress = mUserOp.sender;
773 | } else {
774 | refundAddress = paymaster;
775 | if (context.length > 0) {
776 | actualGasCost = actualGas * gasPrice;
777 | if (mode != IPaymaster.PostOpMode.postOpReverted) {
778 | IPaymaster(paymaster).postOp{
779 | gas: mUserOp.verificationGasLimit
780 | }(mode, context, actualGasCost);
781 | } else {
782 | // solhint-disable-next-line no-empty-blocks
783 | try
784 | IPaymaster(paymaster).postOp{
785 | gas: mUserOp.verificationGasLimit
786 | }(mode, context, actualGasCost)
787 | {} catch Error(string memory reason) {
788 | revert FailedOp(
789 | opIndex,
790 | string.concat("AA50 postOp reverted: ", reason)
791 | );
792 | } catch {
793 | revert FailedOp(opIndex, "AA50 postOp revert");
794 | }
795 | }
796 | }
797 | }
798 | actualGas += preGas - gasleft();
799 | actualGasCost = actualGas * gasPrice;
800 | if (opInfo.prefund < actualGasCost) {
801 | revert FailedOp(opIndex, "AA51 prefund below actualGasCost");
802 | }
803 | uint256 refund = opInfo.prefund - actualGasCost;
804 | _incrementDeposit(refundAddress, refund);
805 | bool success = mode == IPaymaster.PostOpMode.opSucceeded;
806 | emit UserOperationEvent(
807 | opInfo.userOpHash,
808 | mUserOp.sender,
809 | mUserOp.paymaster,
810 | mUserOp.nonce,
811 | success,
812 | actualGasCost,
813 | actualGas
814 | );
815 | } // unchecked
816 | }
817 |
818 | /**
819 | * the gas price this UserOp agrees to pay.
820 | * relayer/block builder might submit the TX with higher priorityFee, but the user should not
821 | */
822 | function getUserOpGasPrice(
823 | MemoryUserOp memory mUserOp
824 | ) internal view returns (uint256) {
825 | unchecked {
826 | uint256 maxFeePerGas = mUserOp.maxFeePerGas;
827 | uint256 maxPriorityFeePerGas = mUserOp.maxPriorityFeePerGas;
828 | if (maxFeePerGas == maxPriorityFeePerGas) {
829 | //legacy mode (for networks that don't support basefee opcode)
830 | return maxFeePerGas;
831 | }
832 | return min(maxFeePerGas, maxPriorityFeePerGas + block.basefee);
833 | }
834 | }
835 |
836 | function min(uint256 a, uint256 b) internal pure returns (uint256) {
837 | return a < b ? a : b;
838 | }
839 |
840 | function getOffsetOfMemoryBytes(
841 | bytes memory data
842 | ) internal pure returns (uint256 offset) {
843 | assembly {
844 | offset := data
845 | }
846 | }
847 |
848 | function getMemoryBytesFromOffset(
849 | uint256 offset
850 | ) internal pure returns (bytes memory data) {
851 | assembly {
852 | data := offset
853 | }
854 | }
855 |
856 | //place the NUMBER opcode in the code.
857 | // this is used as a marker during simulation, as this OP is completely banned from the simulated code of the
858 | // account and paymaster.
859 | function numberMarker() internal view {
860 | assembly {
861 | mstore(0, number())
862 | }
863 | }
864 | }
865 |
--------------------------------------------------------------------------------