├── 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 | ![icon2](https://github.com/porco-rosso-j/zk-ecdsAA/assets/88586592/847da28a-f24c-4f98-abf8-bab74f30b788) 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 | Screenshot 2023-06-11 at 9 53 19 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 | --------------------------------------------------------------------------------