Merge pull request #223 from porco-rosso-j/anon-aadhaar

Safe AnonAadhaar Plugin
This commit is contained in:
Jake C-T
2024-07-05 09:12:34 -06:00
committed by GitHub
20 changed files with 3830 additions and 6271 deletions

2
.gitmodules vendored
View File

@@ -9,7 +9,7 @@
url = https://github.com/foundry-rs/forge-std
[submodule "packages/plugins/lib/openzeppelin-contracts"]
path = packages/plugins/lib/openzeppelin-contracts
url = https://github.com/openzeppelin/openzeppelin-contracts
url = https://github.com/OpenZeppelin/openzeppelin-contracts
[submodule "packages/plugins/lib/safe-contracts"]
path = packages/plugins/lib/safe-contracts
url = https://github.com/safe-global/safe-contracts

View File

@@ -17,20 +17,27 @@
"devDependencies": {
"@account-abstraction/contracts": "0.7.0",
"@account-abstraction/utils": "^0.6.0",
"@anon-aadhaar/core": "2.2.0",
"@nomicfoundation/hardhat-chai-matchers": "^2.0.0",
"@nomicfoundation/hardhat-ethers": "^3.0.0",
"@nomicfoundation/hardhat-ignition": "^0.15.5",
"@nomicfoundation/hardhat-ignition-ethers": "^0.15.0",
"@nomicfoundation/hardhat-network-helpers": "^1.0.0",
"@nomicfoundation/hardhat-toolbox": "^3.0.0",
"@nomicfoundation/hardhat-verify": "^1.0.0",
"@nomicfoundation/hardhat-toolbox": "^5.0.0",
"@nomicfoundation/hardhat-verify": "^2.0.8",
"@nomicfoundation/ignition-core": "^0.15.5",
"@thehubbleproject/bls": "^0.5.1",
"@typechain/ethers-v6": "^0.4.0",
"@typechain/hardhat": "^8.0.0",
"@typechain/ethers-v6": "^0.5.1",
"@typechain/hardhat": "^9.1.0",
"@types/chai": "^4.2.0",
"@types/circomlibjs": "^0.1.6",
"@types/mocha": ">=9.1.0",
"@types/node": ">=16.0.0",
"@typescript-eslint/eslint-plugin": ">=6.0.0",
"@typescript-eslint/parser": ">=6.0.0",
"@zk-email/helpers": "^6.1.1",
"chai": "^4.2.0",
"circomlibjs": "0.1.7",
"dotenv": "^16.3.1",
"eslint": ">=8.0.0",
"eslint-config-prettier": "^9.0.0",
@@ -39,8 +46,8 @@
"eslint-plugin-import": "^2.28.1",
"eslint-plugin-prettier": "^5.0.0",
"ethers": "^6.4.0",
"hardhat": "^2.17.1",
"hardhat-gas-reporter": "^1.0.8",
"hardhat": "^2.22.6",
"hardhat-gas-reporter": "^2.2.0",
"hardhat-preprocessor": "^0.1.5",
"prettier": "^3.0.3",
"solidity-coverage": "^0.8.0",

View File

@@ -0,0 +1,58 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.7.0 <0.9.0;
pragma abicoder v2;
import {Safe} from "safe-contracts/contracts/Safe.sol";
import {SafeProxyFactory} from "safe-contracts/contracts/proxies/SafeProxyFactory.sol";
import {SafeProxy} from "safe-contracts/contracts/proxies/SafeProxy.sol";
import {EntryPoint} from "account-abstraction/core/EntryPoint.sol";
import {SafeAnonAadhaarPlugin} from "./SafeAnonAadhaarPlugin.sol";
/*//////////////////////////////////////////////////////////////////////////
THIS CONTRACT IS STILL IN ACTIVE DEVELOPMENT. NOT FOR PRODUCTION USE
//////////////////////////////////////////////////////////////////////////*/
contract SafeAnonAadhaarFactory {
function create(
Safe safeSingleton,
EntryPoint entryPoint,
address owner,
uint256 saltNonce,
address _anonAadhaarAddr,
uint256 _userDataHash
) external returns (SafeAnonAadhaarPlugin) {
bytes32 salt = keccak256(abi.encodePacked(owner, saltNonce));
Safe safe = Safe(
payable(new SafeProxy{salt: salt}(address(safeSingleton)))
);
address[] memory owners = new address[](1);
owners[0] = owner;
SafeAnonAadhaarPlugin plugin = new SafeAnonAadhaarPlugin{salt: salt}(
address(entryPoint),
_anonAadhaarAddr,
address(safe),
_userDataHash
);
safe.setup(
owners,
1,
address(plugin),
abi.encodeCall(
SafeAnonAadhaarPlugin.enableMyself,
(owner, _userDataHash)
),
address(plugin),
address(0),
0,
payable(address(0))
);
return SafeAnonAadhaarPlugin(address(safe));
}
}

View File

@@ -0,0 +1,165 @@
// SPDX-License-Identifier: LGPL-3.0-only
pragma solidity >=0.7.0 <0.9.0;
pragma abicoder v2;
import {Safe4337Base, SIG_VALIDATION_FAILED} from "./utils/Safe4337Base.sol";
import {IEntryPoint, PackedUserOperation} from "account-abstraction/interfaces/IEntryPoint.sol";
import {IAnonAadhaar} from "./utils/anonAadhaar/interfaces/IAnonAadhaar.sol";
interface ISafe {
function enableModule(address module) external;
function execTransactionFromModule(
address to,
uint256 value,
bytes memory data,
uint8 operation
) external returns (bool success);
}
struct AnonAadhaarOwnerStorage {
address owner;
uint256 userDataHash; // the hash of unique and private user data extracted from Aadhaar QR code
}
/*//////////////////////////////////////////////////////////////////////////
THIS CONTRACT IS STILL IN ACTIVE DEVELOPMENT. NOT FOR PRODUCTION USE
//////////////////////////////////////////////////////////////////////////*/
contract SafeAnonAadhaarPlugin is Safe4337Base {
// Should be made possible to enable this if not the last mapping
// mapping(address => mapping(uint => bool)) public signalNullifiers;
mapping(address => AnonAadhaarOwnerStorage) public anonAadhaarOwnerStorage;
address public immutable myAddress; // Module address
address private immutable _entryPoint;
address internal constant _SENTINEL_MODULES = address(0x1);
// external contract managed by Anon Aadhaar with verifyAnonAadhaarProof() method
// set to immutable to bypass invalid storage access error and make it accessible via delegatecall.
address public immutable anonAadhaarAddr;
// nullifier for each signal(userOpHash) to prevent on-chain front-running
// mapping(uint => bool) public signalNullifiers;
event OWNER_UPDATED(
address indexed safe,
address indexed oldOwner,
address indexed newOwner
);
constructor(
address entryPointAddress,
address _anonAadhaarAddr,
address _safe,
uint256 _userDataHash
) {
myAddress = address(this);
_entryPoint = entryPointAddress;
anonAadhaarAddr = _anonAadhaarAddr;
anonAadhaarOwnerStorage[_safe].userDataHash = _userDataHash;
}
function getOwner(address safe) external view returns (address owner) {
owner = anonAadhaarOwnerStorage[safe].owner;
}
function getUserDataHash(
address safe
) external view returns (uint userDataHash) {
userDataHash = anonAadhaarOwnerStorage[safe].userDataHash;
}
function execTransaction(
address to,
uint256 value,
bytes calldata data
) external payable {
_requireFromEntryPoint();
bool success = _currentSafe().execTransactionFromModule(
to,
value,
data,
0
);
require(success, "tx failed");
}
function enableMyself(address ownerKey, uint256 userDataHash) public {
// Called during safe setup as a delegatecall. This is why we use `this`
// to refer to the safe instead of `msg.sender` / _currentSafe().
ISafe(address(this)).enableModule(myAddress);
// Enable the safe address with the defined key
// bytes memory _data = abi.encodePacked(ownerKey, userDataHash);
bytes memory _data = abi.encode(ownerKey, userDataHash);
SafeAnonAadhaarPlugin(myAddress).enable(_data);
}
function entryPoint() public view override returns (IEntryPoint) {
return IEntryPoint(_entryPoint);
}
function enable(bytes calldata _data) external payable {
// address newOwner = address(bytes20(_data[0:20]));
(address newOwner, uint256 userDataHash) = abi.decode(
_data,
(address, uint)
);
address oldOwner = anonAadhaarOwnerStorage[msg.sender].owner;
anonAadhaarOwnerStorage[msg.sender].owner = newOwner;
anonAadhaarOwnerStorage[msg.sender].userDataHash = userDataHash;
emit OWNER_UPDATED(msg.sender, oldOwner, newOwner);
}
/// @dev Check if the timestamp is more recent than (current time - 3 hours)
/// @param timestamp: msg.sender address.
/// @return bool
function isLessThan3HoursAgo(uint timestamp) public view returns (bool) {
return timestamp > (block.timestamp - 3 * 60 * 60);
}
function _validateSignature(
PackedUserOperation calldata userOp,
bytes32 userOpHash
) internal view override returns (uint256 validationData) {
// decode proof verification params
(
uint256 nullifierSeed,
uint256 timestamp,
uint256 signal,
uint[4] memory revealArray,
uint[8] memory groth16Proof
) = abi.decode(userOp.signature, (uint, uint, uint, uint[4], uint[8]));
// Check if the signal value has already been nullified
// require(!signalNullifiers[signal], "DUPLICATED_NULLIFIER");
// make sure userOpHash == signal
require(uint(userOpHash) == signal, "INVALID_SIGNAL");
// see if the proof is fresh enough
// not called to avoid invalid opcode: the use of block.timestamp.
// require(isLessThan3HoursAgo(timestamp), "INVALID_TIMESTAMP");
// verify proof throuugh AnonAadhaar and AnonAadhaarGroth16Verifier contracts
if (
!IAnonAadhaar(anonAadhaarAddr).verifyAnonAadhaarProof(
nullifierSeed,
anonAadhaarOwnerStorage[userOp.sender].userDataHash,
timestamp,
signal,
revealArray,
groth16Proof
)
) {
return SIG_VALIDATION_FAILED;
}
// signalNullifiers[userOp.sender][signal] = true; // store nullifier
return 0;
}
}

View File

@@ -0,0 +1,77 @@
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.19;
import "./interfaces/IAnonAadhaarGroth16Verifier.sol";
import "./interfaces/IAnonAadhaar.sol";
// Note: This is a AnonAadhaar contract with a modification that made`verifier` state variable immutable
// so that verification doesn't fail due to invalid storage access.
// https://github.com/anon-aadhaar/anon-aadhaar/blob/main/packages/contracts/src/AnonAadhaar.sol
contract AnonAadhaar is IAnonAadhaar {
address public immutable verifier;
uint256 public immutable storedPublicKeyHash;
constructor(address _verifier, uint256 _pubkeyHash) {
verifier = _verifier;
storedPublicKeyHash = _pubkeyHash;
}
/// @dev Verifies that the public key received is corresponding with the one stored in the contract.
/// @param _receivedpubkeyHash: Public key received.
/// @return Verified bool
function verifyPublicKeyHash(
uint256 _receivedpubkeyHash
) private view returns (bool) {
return storedPublicKeyHash == _receivedpubkeyHash;
}
/// @dev Verifies the AnonAadhaar proof received.
/// @param nullifier: Nullifier for the users Aadhaar.
/// @param timestamp: Timestamp of when the QR code was signed.
/// @param signal: Signal committed while genereting the proof.
/// @param revealArray: Array of the values used as input for the proof generation (equal to [0, 0, 0, 0] if no field reveal were asked).
/// @param groth16Proof: SNARK Groth16 proof.
/// @return Verified bool
function verifyAnonAadhaarProof(
uint nullifierSeed,
uint nullifier,
uint timestamp,
uint signal,
uint[4] memory revealArray,
uint[8] memory groth16Proof
) public view returns (bool) {
uint signalHash = _hash(signal);
return
IAnonAadhaarGroth16Verifier(verifier).verifyProof(
[groth16Proof[0], groth16Proof[1]],
[
[groth16Proof[2], groth16Proof[3]],
[groth16Proof[4], groth16Proof[5]]
],
[groth16Proof[6], groth16Proof[7]],
[
storedPublicKeyHash,
nullifier,
timestamp,
// revealAgeAbove18
revealArray[0],
// revealGender
revealArray[1],
// revealPincode
revealArray[2],
// revealState
revealArray[3],
nullifierSeed,
signalHash
]
);
}
/// @dev Creates a keccak256 hash of a message compatible with the SNARK scalar modulus.
/// @param message: Message to be hashed.
/// @return Message digest.
function _hash(uint256 message) private pure returns (uint256) {
return uint256(keccak256(abi.encodePacked(message))) >> 3;
}
}

View File

@@ -0,0 +1,277 @@
// SPDX-License-Identifier: GPL-3.0
/*
Copyright 2021 0KIMS association.
This file is generated with [snarkJS](https://github.com/iden3/snarkjs).
snarkJS is a free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
snarkJS is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
License for more details.
You should have received a copy of the GNU General Public License
along with snarkJS. If not, see <https://www.gnu.org/licenses/>.
*/
pragma solidity >=0.7.0 <0.9.0;
contract AnonAadhaarVerifier {
// Scalar field size
uint256 constant r =
21888242871839275222246405745257275088548364400416034343698204186575808495617;
// Base field size
uint256 constant q =
21888242871839275222246405745257275088696311157297823662689037894645226208583;
// Verification Key data
uint256 constant alphax =
20491192805390485299153009773594534940189261866228447918068658471970481763042;
uint256 constant alphay =
9383485363053290200918347156157836566562967994039712273449902621266178545958;
uint256 constant betax1 =
4252822878758300859123897981450591353533073413197771768651442665752259397132;
uint256 constant betax2 =
6375614351688725206403948262868962793625744043794305715222011528459656738731;
uint256 constant betay1 =
21847035105528745403288232691147584728191162732299865338377159692350059136679;
uint256 constant betay2 =
10505242626370262277552901082094356697409835680220590971873171140371331206856;
uint256 constant gammax1 =
11559732032986387107991004021392285783925812861821192530917403151452391805634;
uint256 constant gammax2 =
10857046999023057135944570762232829481370756359578518086990519993285655852781;
uint256 constant gammay1 =
4082367875863433681332203403145435568316851327593401208105741076214120093531;
uint256 constant gammay2 =
8495653923123431417604973247489272438418190587263600148770280649306958101930;
uint256 constant deltax1 =
4299544762320140490788925402007646552975895862668459356441621010020815213047;
uint256 constant deltax2 =
17871422848268104931114959135211373177342070617197528523406495979941661483095;
uint256 constant deltay1 =
15409138591596798216491700390363405976448424349216823319603684880497172470392;
uint256 constant deltay2 =
6760351807715958749139905882704408015750124381329342019530380383696940656228;
uint256 constant IC0x =
8193099715697298773004274364923775437331207408810138294417049334729397568044;
uint256 constant IC0y =
16159858748150857247889758127444994458965923852898118281816624376217211565521;
uint256 constant IC1x =
13108625264357966313259920811838652543870024141705807775281772752705667110553;
uint256 constant IC1y =
2544332662962865223088426787295310199726639257423304707272531378194627250471;
uint256 constant IC2x =
12195716830305360975540216000332921077692646069403551236428029555879142592435;
uint256 constant IC2y =
13326636634666739773620233113286098727293935511084177100928164174534804438393;
uint256 constant IC3x =
2688577362875587400781907970343221084288450779702134055253900789420295472719;
uint256 constant IC3y =
2113388700188757742720394931373303194111138693822901565764110433637094759188;
uint256 constant IC4x =
19945525246931746661643582268635665599397781283994833186263056491807948286051;
uint256 constant IC4y =
16073805640677310775936718905838964579449451142346916951664145755535936866626;
uint256 constant IC5x =
15285986370760482985887872526745945510369406463801327538950916850999926460800;
uint256 constant IC5y =
11338735502852181248738133285261397537982982234234692816858010242555671562990;
uint256 constant IC6x =
16599939912082694861350909874825011692719127627200428021965770460961325896451;
uint256 constant IC6y =
2023372250411688527623252878305455588947803700538403282505116393326811329911;
uint256 constant IC7x =
937704650640100783774452178387730049849091986214664695849468839894667227986;
uint256 constant IC7y =
1528238522480536593837231193324408021036867916082543038928582346279983380311;
uint256 constant IC8x =
20307194658033847757064139044228229338386028222602331026142092196761205728977;
uint256 constant IC8y =
20906962585416997063272305175664219300089590012260266224825000463561652784804;
uint256 constant IC9x =
765061429630449224592522816482257405186943062678395274581816041425416367436;
uint256 constant IC9y =
2812008994701470589234587891573790293274884176359538091522378449656962019510;
// Memory data
uint16 constant pVk = 0;
uint16 constant pPairing = 128;
uint16 constant pLastMem = 896;
function verifyProof(
uint[2] calldata _pA,
uint[2][2] calldata _pB,
uint[2] calldata _pC,
uint[9] calldata _pubSignals
) public view returns (bool) {
assembly {
function checkField(v) {
if iszero(lt(v, r)) {
mstore(0, 0)
return(0, 0x20)
}
}
// G1 function to multiply a G1 value(x,y) to value in an address
function g1_mulAccC(pR, x, y, s) {
let success
let mIn := mload(0x40)
mstore(mIn, x)
mstore(add(mIn, 32), y)
mstore(add(mIn, 64), s)
// `sub(gas(), 2000` replaced with `gas()` to avoid invalid opcode usage
// success := staticcall(sub(gas(), 2000), 7, mIn, 96, mIn, 64)
success := staticcall(gas(), 7, mIn, 96, mIn, 64)
if iszero(success) {
mstore(0, 0)
return(0, 0x20)
}
mstore(add(mIn, 64), mload(pR))
mstore(add(mIn, 96), mload(add(pR, 32)))
// `sub(gas(), 2000` replaced with `gas()` to avoid invalid opcode usage
// success := staticcall(sub(gas(), 2000), 6, mIn, 128, pR, 64)
success := staticcall(gas(), 6, mIn, 128, pR, 64)
if iszero(success) {
mstore(0, 0)
return(0, 0x20)
}
}
function checkPairing(pA, pB, pC, pubSignals, pMem) -> isOk {
let _pPairing := add(pMem, pPairing)
let _pVk := add(pMem, pVk)
mstore(_pVk, IC0x)
mstore(add(_pVk, 32), IC0y)
// Compute the linear combination vk_x
g1_mulAccC(_pVk, IC1x, IC1y, calldataload(add(pubSignals, 0)))
g1_mulAccC(_pVk, IC2x, IC2y, calldataload(add(pubSignals, 32)))
g1_mulAccC(_pVk, IC3x, IC3y, calldataload(add(pubSignals, 64)))
g1_mulAccC(_pVk, IC4x, IC4y, calldataload(add(pubSignals, 96)))
g1_mulAccC(_pVk, IC5x, IC5y, calldataload(add(pubSignals, 128)))
g1_mulAccC(_pVk, IC6x, IC6y, calldataload(add(pubSignals, 160)))
g1_mulAccC(_pVk, IC7x, IC7y, calldataload(add(pubSignals, 192)))
g1_mulAccC(_pVk, IC8x, IC8y, calldataload(add(pubSignals, 224)))
g1_mulAccC(_pVk, IC9x, IC9y, calldataload(add(pubSignals, 256)))
// -A
mstore(_pPairing, calldataload(pA))
mstore(
add(_pPairing, 32),
mod(sub(q, calldataload(add(pA, 32))), q)
)
// B
mstore(add(_pPairing, 64), calldataload(pB))
mstore(add(_pPairing, 96), calldataload(add(pB, 32)))
mstore(add(_pPairing, 128), calldataload(add(pB, 64)))
mstore(add(_pPairing, 160), calldataload(add(pB, 96)))
// alpha1
mstore(add(_pPairing, 192), alphax)
mstore(add(_pPairing, 224), alphay)
// beta2
mstore(add(_pPairing, 256), betax1)
mstore(add(_pPairing, 288), betax2)
mstore(add(_pPairing, 320), betay1)
mstore(add(_pPairing, 352), betay2)
// vk_x
mstore(add(_pPairing, 384), mload(add(pMem, pVk)))
mstore(add(_pPairing, 416), mload(add(pMem, add(pVk, 32))))
// gamma2
mstore(add(_pPairing, 448), gammax1)
mstore(add(_pPairing, 480), gammax2)
mstore(add(_pPairing, 512), gammay1)
mstore(add(_pPairing, 544), gammay2)
// C
mstore(add(_pPairing, 576), calldataload(pC))
mstore(add(_pPairing, 608), calldataload(add(pC, 32)))
// delta2
mstore(add(_pPairing, 640), deltax1)
mstore(add(_pPairing, 672), deltax2)
mstore(add(_pPairing, 704), deltay1)
mstore(add(_pPairing, 736), deltay2)
// `sub(gas(), 2000` replaced with `gas()` to avoid invalid opcode usage
// let success := staticcall(sub(gas(), 2000), 8, _pPairing, 768, _pPairing, 0x20)
let success := staticcall(
gas(),
8,
_pPairing,
768,
_pPairing,
0x20
)
isOk := and(success, mload(_pPairing))
}
let pMem := mload(0x40)
mstore(0x40, add(pMem, pLastMem))
// Validate that all evaluations ∈ F
checkField(calldataload(add(_pubSignals, 0)))
checkField(calldataload(add(_pubSignals, 32)))
checkField(calldataload(add(_pubSignals, 64)))
checkField(calldataload(add(_pubSignals, 96)))
checkField(calldataload(add(_pubSignals, 128)))
checkField(calldataload(add(_pubSignals, 160)))
checkField(calldataload(add(_pubSignals, 192)))
checkField(calldataload(add(_pubSignals, 224)))
checkField(calldataload(add(_pubSignals, 256)))
checkField(calldataload(add(_pubSignals, 288)))
// Validate all evaluations
let isValid := checkPairing(_pA, _pB, _pC, _pubSignals, pMem)
mstore(0, isValid)
return(0, 0x20)
}
}
}

View File

@@ -0,0 +1,5 @@
# Anon Aadhaar Contracts
These contracts are copied from https://github.com/anon-aadhaar/anon-aadhaar/tree/main/packages/contracts @ `v2.2.0`. This has been done for 2 reasons:
- `AnonAadhaarVerifier.sol` needed to be modified to use gas opcodes that work within the [validation cycle gas opcode limitations for ERC-4337 (OP-012)](https://eips.ethereum.org/EIPS/eip-7562#opcode-rules).
- Using the `@anon-aadhaar/contracts` npm package's contract interfaces causes issues with Typechain generation in this project.

View File

@@ -0,0 +1,13 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.19;
interface IAnonAadhaar {
function verifyAnonAadhaarProof(
uint nullifierSeed,
uint nullifier,
uint timestamp,
uint signal,
uint[4] memory revealArray,
uint[8] memory groth16Proof
) external view returns (bool);
}

View File

@@ -0,0 +1,11 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.19;
interface IAnonAadhaarGroth16Verifier {
function verifyProof(
uint[2] calldata _pA,
uint[2][2] calldata _pB,
uint[2] calldata _pC,
uint[9] calldata publicInputs
) external view returns (bool);
}

View File

@@ -17,3 +17,8 @@ import {Safe} from "safe-contracts/contracts/Safe.sol";
import {EntryPoint} from "account-abstraction/core/EntryPoint.sol";
import {SimpleAccountFactory} from "account-abstraction/samples/SimpleAccountFactory.sol";
import {BLSSignatureAggregator} from "account-abstraction/samples/bls/BLSSignatureAggregator.sol";
// Anon-Aadhaar
import {AnonAadhaar} from "./anonAadhaar/AnonAadhaar.sol";
import {AnonAadhaarVerifier} from "./anonAadhaar/AnonAadhaarVerifier.sol";

View File

@@ -0,0 +1,232 @@
/* eslint-disable @typescript-eslint/comma-dangle */
/* eslint-disable prettier/prettier */
import { expect } from "chai";
import { JsonRpcProvider, NonceManager, Signer, ethers } from "ethers";
import DeterministicDeployer from "../../lib-ts/deterministic-deployer/DeterministicDeployer";
import {
SafeAnonAadhaarFactory__factory,
SafeAnonAadhaarPlugin__factory,
AnonAadhaarVerifier__factory,
AnonAadhaar__factory,
Safe,
AnonAadhaar,
} from "../../typechain-types";
import receiptOf from "./utils/receiptOf";
import { setupTests } from "./utils/setupTests";
import {
createAnonAadhaarOperation,
sendUserOpWithAnonAadhaarSig,
} from "./utils/createUserOp";
import {
InitArgs,
init,
generateArgs,
prove,
artifactUrls,
packGroth16Proof,
ArtifactsOrigin,
} from "@anon-aadhaar/core";
import fs from "fs";
import { getUserOpHash } from "./utils/userOpUtils";
import { copmuteUserNullifier } from "./utils/computeNullifier";
import { testQRData } from "./utils/assets/dataInput.json";
/*
This uses test Aadhaar QR code along with test Indian government's public key and certificate.
*/
// Nullifier seed: https://anon-aadhaar-documentation.vercel.app/docs/nullifiers
// using wallet address makes sense...?
const nullifierSeed = 1234;
// test version of UIDAI public key
// more info: https://anon-aadhaar-documentation.vercel.app/docs/how-does-it-work#1-extract-and-process-the-data-from-the-qr-code
const testPublicKeyHash =
"15134874015316324267425466444584014077184337590635665158241104437045239495873";
describe("SafeAnonAadhaarPlugin", () => {
let bundlerProvider: JsonRpcProvider;
let provider: JsonRpcProvider;
let admin: NonceManager;
let owner: Signer;
let entryPointAddress: string;
let safeSingleton: Safe;
let deployer: DeterministicDeployer;
let certificate: string;
let anonAadhaarAddress: string;
let anonAadhaar: AnonAadhaar;
before(async () => {
const setup = await setupTests();
({
provider,
bundlerProvider,
admin,
owner,
entryPointAddress,
deployer,
safeSingleton,
} = setup);
const signer = await provider.getSigner();
// Deploy AnonAadhaarGroth16Verifier contract
const anonAadhaarVerifier = await new AnonAadhaarVerifier__factory(signer).deploy();
await anonAadhaarVerifier.waitForDeployment();
// Deploy AnonAadhaar contract
anonAadhaar = await new AnonAadhaar__factory(signer).deploy(
await anonAadhaarVerifier.getAddress(),
BigInt(testPublicKeyHash).toString(),
);
await anonAadhaar.waitForDeployment();
anonAadhaarAddress = await anonAadhaar.getAddress();
// load test certificate
const certificateDirName = __dirname + "/utils/assets";
certificate = fs
.readFileSync(certificateDirName + "/testCertificate.pem")
.toString();
// load files needed for proof generation and verification
const anonAadhaarInitArgs: InitArgs = {
wasmURL: artifactUrls.v2.wasm,
zkeyURL: artifactUrls.v2.zkey,
vkeyURL: artifactUrls.v2.vk,
artifactsOrigin: ArtifactsOrigin.server,
};
// pass initArgs
await init(anonAadhaarInitArgs);
});
it("should pass the ERC4337 validation", async () => {
// Deploy SafeAnonAadhaarFactory contract
const safeAnonAadhaarFactory = await deployer.connectOrDeploy(
SafeAnonAadhaarFactory__factory,
[]
);
// get userDataHash out of nullifier seed and test QR data
// userDataHash is an unique identifier that is specific to each user and stored the plugin contract
const userDataHash = await copmuteUserNullifier(nullifierSeed, testQRData);
const createArgs = [
safeSingleton,
entryPointAddress,
await owner.getAddress(),
0,
anonAadhaarAddress,
userDataHash,
] satisfies Parameters<typeof safeAnonAadhaarFactory.create.staticCall>;
const accountAddress = await safeAnonAadhaarFactory.create.staticCall(
...createArgs
);
await receiptOf(safeAnonAadhaarFactory.create(...createArgs));
const safeAnonAadhaarPlugin = SafeAnonAadhaarPlugin__factory.connect(
accountAddress,
owner
);
// Native tokens for the pre-fund
await receiptOf(
admin.sendTransaction({
to: accountAddress,
value: ethers.parseEther("10"),
})
);
const recipient = ethers.Wallet.createRandom();
const transferAmount = ethers.parseEther("1");
// Construct userOp
const userOpCallData = safeAnonAadhaarPlugin.interface.encodeFunctionData(
"execTransaction",
[recipient.address, transferAmount, "0x00"]
);
// create User Operation
const unsignedUserOperation = await createAnonAadhaarOperation(
provider,
accountAddress,
userOpCallData,
entryPointAddress
);
// get userOpHash
const userOpHash = getUserOpHash(
unsignedUserOperation,
entryPointAddress,
Number((await provider.getNetwork()).chainId)
);
// prove with userOpHash as signal
const args = await generateArgs({
qrData: testQRData,
certificateFile: certificate,
nullifierSeed: nullifierSeed,
signal: userOpHash, // user op hash
});
// proving
console.debug("Generating Anon Aadhaar proof. This could take some time...");
const proofTimingKey = "Anon Aadhaar proof generation time";
console.time(proofTimingKey);
const anonAadhaarCore = await prove(args);
console.timeEnd(proofTimingKey);
const anonAadhaarProof = anonAadhaarCore.proof;
const packedGroth16Proof = packGroth16Proof(anonAadhaarProof.groth16Proof);
// view call to AnonAadhaar contract to see if verification returns true
expect(await anonAadhaar.verifyAnonAadhaarProof(
nullifierSeed,
anonAadhaarProof.nullifier,
anonAadhaarProof.timestamp,
userOpHash,
[
anonAadhaarProof.ageAbove18,
anonAadhaarProof.gender,
anonAadhaarProof.pincode,
anonAadhaarProof.state,
],
packedGroth16Proof
)).to.equal(true);
// encode proof data into userOpSignature
const encoder = ethers.AbiCoder.defaultAbiCoder();
const userOpSignature = encoder.encode(
["uint", "uint", "uint", "uint[4]", "uint[8]"],
[
BigInt(nullifierSeed),
Number(anonAadhaarProof.timestamp),
BigInt(userOpHash), // insert userOpHash into signature
[
anonAadhaarProof.ageAbove18,
anonAadhaarProof.gender,
anonAadhaarProof.pincode,
anonAadhaarProof.state,
],
packedGroth16Proof,
]
);
// send userOp
await sendUserOpWithAnonAadhaarSig(
bundlerProvider,
entryPointAddress,
unsignedUserOperation,
userOpSignature
);
expect(await provider.getBalance(recipient.address)).to.equal(
ethers.parseEther("1")
);
}).timeout(0);
});

View File

@@ -25,14 +25,12 @@ describe("SafeSponsorEverythingPaymasterPlugin", () => {
} = await setupTests();
// Deploy paymaster.
const paymaster = await deployer.connectOrDeploy(
SponsorEverythingPaymaster__factory,
[entryPointAddress],
);
const paymaster = await new SponsorEverythingPaymaster__factory(admin).deploy(entryPointAddress);
await paymaster.waitForDeployment();
const paymasterAddress = await paymaster.getAddress();
// Paymaster deposits.
await paymaster.deposit({ value: oneEther })
await paymaster.connect(admin).deposit({ value: oneEther })
const recipient = ethers.Wallet.createRandom();
const transferAmount = oneEther;

View File

@@ -0,0 +1,3 @@
{
"testQRData": "8259163575998395410294216884136380576185817320339145460288951755287582961380611852552428987321584902318624273479337130653734982789439199350807739714406680256506601030028361685736660257517232716829232450159251789263870750283214820475102793105777087762238893090228084052270739203426767272062178826235941508196284529472654271516164224874687419158221021213944829682919423174703783469927383220474654008065915029614141226522064062660593170425792840873655513538373377850112144063189928583588899889878172757870400281696669604010659786496608127700010264443115263361656744433002559396889060190428705316366290450741550935385486607346514118464415324976934593027192262025619948063647667007927187736245772179085671658409804311603784752615097922989017361163561315974008304022542448394278143245816470881130080719485003834016131185071765229491892891069788319670287394271744730364788949609836924781874523936880888005883165757273872375006288978183466996520618718348187182821516617721340861010989807614756396013627238651856164981477576514065364628430139194213240602981419233621531616776712580234318576148789862972873366521755587675635811636464535551028275057950562020714225333126426609311459495088802145911084644641596208432517247324679678535859879970296810837735288916946197174410518342751033634782712968162882714769666441813893046220965525694847349131353986974388432968669605721975441870936552792275255624723251162192468002453471184713983574359601113515796454264270501379717344206777921353459767049560942843350534472442799601294637063232419543855742825887931841338302499933012059977947394755335155868283405337181095220998277373266658634859632929226320059674299759100792654417315629048732480315019941928105082550091217622422743467170706956093632228513797781797454779203616427853022505097310749994766657051986303478622173767936568165644251615127773430128638507677775244195799780291921828512257290767451475181728141544788756907393883042588060697683541401090581157249784874529424005078918452607589129440476242749110421616270676359722523229311894327359615548588038186027827017569331332262329182564217789843145105509621002324556840213928256545454178891208004109769624959566302976213521762873815749009289995208912424872527724417047936432945498377307452190302923489092664437908497749093491199476080757200233878726847198496754472664256996743796092233459542884818717466621372105594672115988382565552756801323160697003960485232732393383241422077506009076922303757067128564302338914230360252223406874457414109774901980252709597099278192874164252010830754720603092419792069707099362278082792090307065378744856387301364608460967253691290230861162587170799141457093188189022390589265654613500974699477990974878105678883229707694455342266695530373994049224098435972125150350136428446936271698977517627416435999970351222450833295217051468307037908262231982382247410542334757724852032521780157518474618653527191342825230455100778913195115477763082159513429761573752871477695723697689470263993132596482716347199834315782099668846081963760553679915994617396376870314998926788197388410764535427795200340714967872713095483294486407886767431404892448155562283436571050452251042117926586451385682519188252281397"
}

View File

@@ -0,0 +1,23 @@
-----BEGIN CERTIFICATE-----
MIID6jCCAtKgAwIBAgIBATANBgkqhkiG9w0BAQsFADBpMRQwEgYDVQQDEwtleGFt
cGxlLm9yZzELMAkGA1UEBhMCVVMxETAPBgNVBAgTCFZpcmdpbmlhMRMwEQYDVQQH
EwpCbGFja3NidXJnMQ0wCwYDVQQKEwRUZXN0MQ0wCwYDVQQLEwRUZXN0MB4XDTI0
MDMwMTE2MTc0MFoXDTI1MDMwMTE2MTc0MFowaTEUMBIGA1UEAxMLZXhhbXBsZS5v
cmcxCzAJBgNVBAYTAlVTMREwDwYDVQQIEwhWaXJnaW5pYTETMBEGA1UEBxMKQmxh
Y2tzYnVyZzENMAsGA1UEChMEVGVzdDENMAsGA1UECxMEVGVzdDCCASIwDQYJKoZI
hvcNAQEBBQADggEPADCCAQoCggEBAJXoH3UGWVzCYq3c/d012AQFWyrfRtxxnFa1
ju4ZmZXvoaMlRxBiCseAHpdvROO+RU2w8ZDj99TjWYlyEXNE3lL894JvhJSIqVmn
s9Iett0DRRZi6og+7u/eiJoUmbmkf5UExfCWwmK5bSPRl1AzLZ6X62FB0mHel5lN
TEFjypy+PgdyIbRCU9z4FglCi2g1HuPptg0rNR/apu6MKKhFI5+X3nzA/l0UTkdI
E/tD7Fg/gbTuMowiFnM0iY0hC6AXom7GiUDwXfIr2cyGu8OkNUOSNy1WYWd2m3Nb
oSyjWA+RnBvYunDEwqsKzysJvC+umB88ApWm4enySPUAcwlP+vECAwEAAaOBnDCB
mTAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIC9DA7BgNVHSUENDAyBggrBgEFBQcD
AQYIKwYBBQUHAwIGCCsGAQUFBwMDBggrBgEFBQcDBAYIKwYBBQUHAwgwEQYJYIZI
AYb4QgEBBAQDAgD3MCwGA1UdEQQlMCOGG2h0dHA6Ly9leGFtcGxlLm9yZy93ZWJp
ZCNtZYcEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEAjDnIK8R7ruSiJpdUeZ2ZxaGI
ewf020sAc0ENw6x3jaKKFINNLnMqEKLntZzIg7WbjnKLhtUhb/SLHP59mfT2OJVS
YMW7uRmnajfEd0MzfrXIUKBwCeWTCyP/bcdlJOT4u24ngFnkUyWaEKnqH3YuI8cw
BKWQ26Lq7ODq6e6Otxf0KOtWTeVSzlCL66MGBvQ36LF1g8PLEZ9hvvX06ieaE99W
HDK9/pGSuobx6Fi7ufzqymirT/GOuxslquB4mleCu4ArCg2qAVC2wDjQymgvq8FS
bVukYnC6XdwqYhvIuTPnEys4gGdeirY+UPGQeqxMrNm+ZpKPTS0NwzxHwlhutw==
-----END CERTIFICATE-----

View File

@@ -0,0 +1,73 @@
import {
convertBigIntToByteArray,
decompressByteArray,
extractPhoto,
} from "@anon-aadhaar/core";
import { sha256Pad } from "@zk-email/helpers/dist/sha-utils";
import { buildPoseidon } from "circomlibjs";
// Method to extract a nullifier specific to each Aadhaar ID owner from Aadhaar QR code
// https://github.com/anon-aadhaar/anon-aadhaar/blob/main/packages/circuits/test/aadhaar-verifier.test.ts#L140
export async function copmuteUserNullifier(
nullifierSeed: number,
qrData: string
): Promise<bigint> {
const qrDataBytes = convertBigIntToByteArray(BigInt(qrData));
const decodedData = decompressByteArray(qrDataBytes);
const signedData = decodedData.slice(0, decodedData.length - 256);
const [qrDataPadded, qrDataPaddedLen] = sha256Pad(signedData, 512 * 3);
const { bytes: photoBytes } = extractPhoto(
Array.from(qrDataPadded),
qrDataPaddedLen
);
const photoBytesPacked = padArrayWithZeros(
bytesToIntChunks(new Uint8Array(photoBytes), 31),
32
);
const poseidon = await buildPoseidon();
const first16 = poseidon([...photoBytesPacked.slice(0, 16)]);
const last16 = poseidon([...photoBytesPacked.slice(16, 32)]);
const nullifier = poseidon([nullifierSeed, first16, last16]);
return BigInt(poseidon.F.toString(nullifier));
}
export function bytesToIntChunks(
bytes: Uint8Array,
maxBytesInField: number
): bigint[] {
const numChunks = Math.ceil(bytes.length / maxBytesInField);
const ints: bigint[] = new Array(numChunks).fill(BigInt(0));
for (let i = 0; i < numChunks; i++) {
let intSum = BigInt(0);
for (let j = 0; j < maxBytesInField; j++) {
const idx = maxBytesInField * i + j;
if (idx >= bytes.length) break; // Stop if we've processed all bytes
// Shift byte into position and add to current integer sum
intSum += BigInt(bytes[idx]) * BigInt(256) ** BigInt(j);
}
ints[i] = intSum;
}
return ints;
}
export function padArrayWithZeros(
bigIntArray: bigint[],
requiredLength: number
) {
const currentLength = bigIntArray.length;
const zerosToFill = requiredLength - currentLength;
if (zerosToFill > 0) {
return [...bigIntArray, ...Array(zerosToFill).fill(BigInt(0))];
}
return bigIntArray;
}

View File

@@ -1,79 +1,81 @@
/* eslint-disable prettier/prettier */
/* eslint-disable @typescript-eslint/comma-dangle */
import { BigNumberish, BytesLike, ethers, getBytes, NonceManager, Signer } from "ethers";
import { AddressZero } from "@ethersproject/constants";
import { SafeProxyFactory } from "../../../typechain-types/lib/safe-contracts/contracts/proxies/SafeProxyFactory";
import { Safe } from "../../../typechain-types/lib/safe-contracts/contracts/Safe";
import {
EntryPoint__factory,
SafeBlsPlugin,
SafeWebAuthnPlugin,
EntryPoint__factory,
SafeBlsPlugin,
SafeWebAuthnPlugin,
} from "../../../typechain-types";
import receiptOf from "./receiptOf";
import { calculateProxyAddress } from "./calculateProxyAddress";
import { getGasEstimates } from "./getGasEstimates";
import { getFeeData, getGasEstimates } from "./getGasEstimates";
import sendUserOpAndWait from "./sendUserOpAndWait";
import {
FactoryParams,
getUserOpHash,
PackedUserOperation,
packUserOp,
UserOperation,
FactoryParams,
getUserOpHash,
PackedUserOperation,
packUserOp,
UserOperation,
} from "./userOpUtils";
type Plugin = SafeBlsPlugin | SafeWebAuthnPlugin;
export const generateFactoryParamsAndAddress = async (
admin: NonceManager,
owner: NonceManager,
plugin: Plugin,
safeSingleton: Safe,
safeProxyFactory: SafeProxyFactory,
admin: NonceManager,
owner: NonceManager,
plugin: Plugin,
safeSingleton: Safe,
safeProxyFactory: SafeProxyFactory
) => {
const pluginAddress = await plugin.getAddress();
const singletonAddress = await safeSingleton.getAddress();
const factoryAddress = await safeProxyFactory.getAddress();
const pluginAddress = await plugin.getAddress();
const singletonAddress = await safeSingleton.getAddress();
const factoryAddress = await safeProxyFactory.getAddress();
const moduleInitializer = plugin.interface.encodeFunctionData("enableMyself");
const encodedInitializer = safeSingleton.interface.encodeFunctionData(
"setup",
[
[await owner.getAddress()],
1,
pluginAddress,
moduleInitializer,
pluginAddress,
AddressZero,
0,
AddressZero,
],
);
const moduleInitializer = plugin.interface.encodeFunctionData("enableMyself");
const encodedInitializer = safeSingleton.interface.encodeFunctionData(
"setup",
[
[await owner.getAddress()],
1,
pluginAddress,
moduleInitializer,
pluginAddress,
AddressZero,
0,
AddressZero,
]
);
const deployedAddress = await calculateProxyAddress(
safeProxyFactory,
await safeSingleton.getAddress(),
encodedInitializer,
73,
);
const deployedAddress = await calculateProxyAddress(
safeProxyFactory,
await safeSingleton.getAddress(),
encodedInitializer,
73
);
// Native tokens for the pre-fund
await receiptOf(
admin.sendTransaction({
to: deployedAddress,
value: ethers.parseEther("10"),
}),
);
// Native tokens for the pre-fund
await receiptOf(
admin.sendTransaction({
to: deployedAddress,
value: ethers.parseEther("10"),
})
);
const factoryData = safeProxyFactory.interface.encodeFunctionData(
"createProxyWithNonce",
[singletonAddress, encodedInitializer, 73],
);
const factoryData = safeProxyFactory.interface.encodeFunctionData(
"createProxyWithNonce",
[singletonAddress, encodedInitializer, 73]
);
const factoryParams = {
factory: factoryAddress,
factoryData,
};
const factoryParams = {
factory: factoryAddress,
factoryData,
};
return { factoryParams, deployedAddress };
return { factoryParams, deployedAddress };
};
export const createUserOperation = async (
@@ -88,25 +90,25 @@ export const createUserOperation = async (
paymasterPostOpGasLimit?: BigNumberish,
paymasterData?: BytesLike,
) => {
const entryPoint = EntryPoint__factory.connect(
entryPointAddress,
await provider.getSigner(),
);
const nonce = await entryPoint.getNonce(accountAddress, "0x00");
const nonceHex = "0x0" + nonce.toString();
const entryPoint = EntryPoint__factory.connect(
entryPointAddress,
await provider.getSigner()
);
const nonce = await entryPoint.getNonce(accountAddress, "0x00");
const nonceHex = "0x0" + nonce.toString();
let userOp: Partial<UserOperation> = {
sender: accountAddress,
nonce: nonceHex,
callData: userOpCallData,
callGasLimit: "0x00",
signature: dummySignature,
};
let userOp: Partial<UserOperation> = {
sender: accountAddress,
nonce: nonceHex,
callData: userOpCallData,
callGasLimit: "0x00",
signature: dummySignature,
};
if (factoryParams.factory !== "0x") {
userOp.factory = factoryParams.factory;
userOp.factoryData = factoryParams.factoryData;
}
if (factoryParams.factory !== "0x") {
userOp.factory = factoryParams.factory;
userOp.factoryData = factoryParams.factoryData;
}
const {
callGasLimit,
@@ -140,7 +142,7 @@ export const createUserOperation = async (
signature: dummySignature,
} satisfies UserOperation;
return await ethers.resolveProperties(unsignedUserOperation);
return await ethers.resolveProperties(unsignedUserOperation);
};
export const createAndSendUserOpWithEcdsaSig = async (
@@ -169,22 +171,86 @@ export const createAndSendUserOpWithEcdsaSig = async (
paymasterData,
);
const userOpHash = getUserOpHash(
unsignedUserOperation,
entryPointAddress,
Number((await provider.getNetwork()).chainId),
);
const userOpHash = getUserOpHash(
unsignedUserOperation,
entryPointAddress,
Number((await provider.getNetwork()).chainId)
);
const userOpSignature = await owner.signMessage(getBytes(userOpHash));
const userOpSignature = await owner.signMessage(getBytes(userOpHash));
const userOperation = {
...unsignedUserOperation,
signature: userOpSignature,
};
const userOperation = {
...unsignedUserOperation,
signature: userOpSignature,
};
return await sendUserOpAndWait(
userOperation,
entryPointAddress,
bundlerProvider,
);
return await sendUserOpAndWait(
userOperation,
entryPointAddress,
bundlerProvider
);
};
export const createAnonAadhaarOperation = async (
provider: ethers.JsonRpcProvider,
accountAddress: string,
userOpCallData: string,
entryPointAddress: string
) => {
const entryPoint = EntryPoint__factory.connect(
entryPointAddress,
await provider.getSigner()
);
const nonce = await entryPoint.getNonce(accountAddress, "0x00");
const nonceHex = "0x0" + nonce.toString();
const { maxFeePerGas, maxPriorityFeePerGas } = await getFeeData(provider);
// Note that gas amount params, such as callGasLimit, verificationGasLimit, preVerificationGas, are constant.
// This is because it's challenging to have dynamic gas values for userOp due to the following dependency loop issue.
// Doing gas estimation requires a valid signature in the userOp. Otherwise, the estimation fails.
// The signautre is an encoded value of zk-proof and signal (userOp Hash), which means signature needs a complete userOp including gas values.
// If gas values are changed after creating signature, on-chain verification fails as the new userOp hash doesn't match the proof anymore.
// solution 1: re-create proof but this takes too much time
// solution 2: have fixed gas values
// solution 3:
// - gas estimation with dummy sig to get at least dynamic callGasLimit. ( pimlico offers such an API call )
// - keep verificationGasLimit & preVerificationGas fixed as they are more or less constant
// this issue is also problematic when simulation call with valid sig is necessary, e.g. userOp w/ paymaster.
const unsignedUserOperation = {
sender: accountAddress,
nonce: nonceHex,
factory: undefined,
factoryData: undefined,
callData: userOpCallData,
callGasLimit: ethers.toBeHex(150000n),
verificationGasLimit: ethers.toBeHex(1000000n),
preVerificationGas: ethers.toBeHex(200000n),
maxFeePerGas,
maxPriorityFeePerGas,
signature: "0x",
} satisfies UserOperation;
return await ethers.resolveProperties(unsignedUserOperation);
};
export const sendUserOpWithAnonAadhaarSig = async (
bundlerProvider: ethers.JsonRpcProvider,
entryPointAddress: string,
unsignedUserOperation: UserOperation,
userOpSignature: string
) => {
const userOperation = {
...unsignedUserOperation,
signature: userOpSignature,
};
return await sendUserOpAndWait(
userOperation,
entryPointAddress,
bundlerProvider
);
};

View File

@@ -1,11 +1,13 @@
/* eslint-disable @typescript-eslint/comma-dangle */
/* eslint-disable prettier/prettier */
import { ethers } from "ethers";
import { UserOperation } from "./userOpUtils";
export const getGasEstimates = async (
provider: ethers.JsonRpcProvider,
bundlerProvider: ethers.JsonRpcProvider,
partialUserOperation: Partial<UserOperation>,
entryPointAddress: string,
provider: ethers.JsonRpcProvider,
bundlerProvider: ethers.JsonRpcProvider,
partialUserOperation: Partial<UserOperation>,
entryPointAddress: string
) => {
const gasEstimate = (await bundlerProvider.send(
"eth_estimateUserOperationGas",
@@ -17,15 +19,15 @@ export const getGasEstimates = async (
callGasLimit: string;
};
const safeVerificationGasLimit =
BigInt(gasEstimate.verificationGasLimit) +
BigInt(gasEstimate.verificationGasLimit); // + 100% TODO: (merge-ok) why do we have to increase the limit so much for all tests to pass?
const safeVerificationGasLimit =
BigInt(gasEstimate.verificationGasLimit) +
BigInt(gasEstimate.verificationGasLimit); // + 100% TODO: (merge-ok) why do we have to increase the limit so much for all tests to pass?
const safePreVerificationGas =
BigInt(gasEstimate.preVerificationGas) +
BigInt(gasEstimate.preVerificationGas) / 10n; // + 10%
const safePreVerificationGas =
BigInt(gasEstimate.preVerificationGas) +
BigInt(gasEstimate.preVerificationGas) / 10n; // + 10%
const { maxFeePerGas, maxPriorityFeePerGas } = await getFeeData(provider);
const { maxFeePerGas, maxPriorityFeePerGas } = await getFeeData(provider);
return {
callGasLimit: gasEstimate.callGasLimit,
@@ -37,16 +39,16 @@ export const getGasEstimates = async (
};
};
async function getFeeData(provider: ethers.Provider) {
const feeData = await provider.getFeeData();
if (!feeData.maxFeePerGas || !feeData.maxPriorityFeePerGas) {
throw new Error(
"maxFeePerGas or maxPriorityFeePerGas is null or undefined",
);
}
export async function getFeeData(provider: ethers.Provider) {
const feeData = await provider.getFeeData();
if (!feeData.maxFeePerGas || !feeData.maxPriorityFeePerGas) {
throw new Error(
"maxFeePerGas or maxPriorityFeePerGas is null or undefined"
);
}
const maxFeePerGas = "0x" + feeData.maxFeePerGas.toString();
const maxPriorityFeePerGas = "0x" + feeData.maxPriorityFeePerGas.toString();
const maxFeePerGas = "0x" + feeData.maxFeePerGas.toString();
const maxPriorityFeePerGas = "0x" + feeData.maxPriorityFeePerGas.toString();
return { maxFeePerGas, maxPriorityFeePerGas };
return { maxFeePerGas, maxPriorityFeePerGas };
}

View File

@@ -1,11 +1,11 @@
import {
AbiCoder,
BigNumberish,
BytesLike,
concat,
hexlify,
isHexString,
keccak256,
AbiCoder,
BigNumberish,
BytesLike,
concat,
hexlify,
isHexString,
keccak256,
} from "ethers";
import { PackedUserOperationStruct } from "../../../typechain-types/lib/account-abstraction/contracts/core/EntryPoint";
@@ -21,26 +21,26 @@ import { PackedUserOperationStruct } from "../../../typechain-types/lib/account-
export type PackedUserOperation = PackedUserOperationStruct;
export type UserOperation = {
sender: string;
nonce: BigNumberish;
factory?: string;
factoryData?: BytesLike;
callData: BytesLike;
callGasLimit: BigNumberish;
verificationGasLimit: BigNumberish;
preVerificationGas: BigNumberish;
maxFeePerGas: BigNumberish;
maxPriorityFeePerGas: BigNumberish;
paymaster?: string;
paymasterVerificationGasLimit?: BigNumberish;
paymasterPostOpGasLimit?: BigNumberish;
paymasterData?: BytesLike;
signature: BytesLike;
sender: string;
nonce: BigNumberish;
factory?: string;
factoryData?: BytesLike;
callData: BytesLike;
callGasLimit: BigNumberish;
verificationGasLimit: BigNumberish;
preVerificationGas: BigNumberish;
maxFeePerGas: BigNumberish;
maxPriorityFeePerGas: BigNumberish;
paymaster?: string;
paymasterVerificationGasLimit?: BigNumberish;
paymasterPostOpGasLimit?: BigNumberish;
paymasterData?: BytesLike;
signature: BytesLike;
};
export type FactoryParams = {
factory: string;
factoryData?: BytesLike;
factory: string;
factoryData?: BytesLike;
};
/**
@@ -53,17 +53,17 @@ export type FactoryParams = {
* @param chainId
*/
export function getUserOpHash(
op: UserOperation,
entryPoint: string,
chainId: number,
op: UserOperation,
entryPoint: string,
chainId: number
): string {
const userOpHash = keccak256(encodeUserOp(op, true));
const defaultAbiCoder = AbiCoder.defaultAbiCoder();
const enc = defaultAbiCoder.encode(
["bytes32", "address", "uint256"],
[userOpHash, entryPoint, chainId],
);
return keccak256(enc);
const userOpHash = keccak256(encodeUserOp(op, true));
const defaultAbiCoder = AbiCoder.defaultAbiCoder();
const enc = defaultAbiCoder.encode(
["bytes32", "address", "uint256"],
[userOpHash, entryPoint, chainId]
);
return keccak256(enc);
}
/**
@@ -73,122 +73,122 @@ export function getUserOpHash(
* "false" to pack entire UserOp, for calculating the calldata cost of putting it on-chain.
*/
export function encodeUserOp(
op1: PackedUserOperation | UserOperation,
forSignature = true,
op1: PackedUserOperation | UserOperation,
forSignature = true
): string {
// if "op" is unpacked UserOperation, then pack it first, before we ABI-encode it.
// if "op" is unpacked UserOperation, then pack it first, before we ABI-encode it.
let op: PackedUserOperation;
if ("callGasLimit" in op1) {
op = packUserOp(op1);
} else {
op = op1;
}
let op: PackedUserOperation;
if ("callGasLimit" in op1) {
op = packUserOp(op1);
} else {
op = op1;
}
const defaultAbiCoder = AbiCoder.defaultAbiCoder();
if (forSignature) {
return defaultAbiCoder.encode(
[
"address",
"uint256",
"bytes32",
"bytes32",
"bytes32",
"uint256",
"bytes32",
"bytes32",
],
[
op.sender,
op.nonce,
keccak256(op.initCode),
keccak256(op.callData),
op.accountGasLimits,
op.preVerificationGas,
op.gasFees,
keccak256(op.paymasterAndData),
],
);
} else {
// for the purpose of calculating gas cost encode also signature (and no keccak of bytes)
return defaultAbiCoder.encode(
[
"address",
"uint256",
"bytes",
"bytes",
"bytes32",
"uint256",
"bytes32",
"bytes",
"bytes",
],
[
op.sender,
op.nonce,
op.initCode,
op.callData,
op.accountGasLimits,
op.preVerificationGas,
op.gasFees,
op.paymasterAndData,
op.signature,
],
);
}
const defaultAbiCoder = AbiCoder.defaultAbiCoder();
if (forSignature) {
return defaultAbiCoder.encode(
[
"address",
"uint256",
"bytes32",
"bytes32",
"bytes32",
"uint256",
"bytes32",
"bytes32",
],
[
op.sender,
op.nonce,
keccak256(op.initCode),
keccak256(op.callData),
op.accountGasLimits,
op.preVerificationGas,
op.gasFees,
keccak256(op.paymasterAndData),
]
);
} else {
// for the purpose of calculating gas cost encode also signature (and no keccak of bytes)
return defaultAbiCoder.encode(
[
"address",
"uint256",
"bytes",
"bytes",
"bytes32",
"uint256",
"bytes32",
"bytes",
"bytes",
],
[
op.sender,
op.nonce,
op.initCode,
op.callData,
op.accountGasLimits,
op.preVerificationGas,
op.gasFees,
op.paymasterAndData,
op.signature,
]
);
}
}
export function packUserOp(op: UserOperation): PackedUserOperation {
let paymasterAndData: BytesLike;
if (op.paymaster == null) {
paymasterAndData = "0x";
} else {
if (
op.paymasterVerificationGasLimit == null ||
op.paymasterPostOpGasLimit == null
) {
throw new Error("paymaster with no gas limits");
}
paymasterAndData = packPaymasterData(
op.paymaster,
op.paymasterVerificationGasLimit,
op.paymasterPostOpGasLimit,
op.paymasterData,
);
}
let paymasterAndData: BytesLike;
if (op.paymaster == null) {
paymasterAndData = "0x";
} else {
if (
op.paymasterVerificationGasLimit == null ||
op.paymasterPostOpGasLimit == null
) {
throw new Error("paymaster with no gas limits");
}
paymasterAndData = packPaymasterData(
op.paymaster,
op.paymasterVerificationGasLimit,
op.paymasterPostOpGasLimit,
op.paymasterData
);
}
return {
sender: op.sender,
nonce: "0x" + BigInt(op.nonce).toString(16),
initCode:
op.factory == null ? "0x" : concat([op.factory, op.factoryData ?? ""]),
callData: op.callData,
accountGasLimits: packUint(op.verificationGasLimit, op.callGasLimit),
preVerificationGas: "0x" + BigInt(op.preVerificationGas).toString(16),
gasFees: packUint(op.maxPriorityFeePerGas, op.maxFeePerGas),
paymasterAndData,
signature: op.signature,
};
return {
sender: op.sender,
nonce: "0x" + BigInt(op.nonce).toString(16),
initCode:
op.factory == null ? "0x" : concat([op.factory, op.factoryData ?? ""]),
callData: op.callData,
accountGasLimits: packUint(op.verificationGasLimit, op.callGasLimit),
preVerificationGas: "0x" + BigInt(op.preVerificationGas).toString(16),
gasFees: packUint(op.maxPriorityFeePerGas, op.maxFeePerGas),
paymasterAndData,
signature: op.signature,
};
}
export function packPaymasterData(
paymaster: string,
paymasterVerificationGasLimit: BigNumberish,
postOpGasLimit: BigNumberish,
paymasterData?: BytesLike,
paymaster: string,
paymasterVerificationGasLimit: BigNumberish,
postOpGasLimit: BigNumberish,
paymasterData?: BytesLike
): BytesLike {
return concat([
paymaster,
packUint(paymasterVerificationGasLimit, postOpGasLimit),
paymasterData ?? "0x",
]);
return concat([
paymaster,
packUint(paymasterVerificationGasLimit, postOpGasLimit),
paymasterData ?? "0x",
]);
}
export function packUint(high128: BigNumberish, low128: BigNumberish): string {
high128 = BigInt(high128);
low128 = BigInt(low128);
const packed = "0x" + ((high128 << 128n) + low128).toString(16);
return hexZeroPad(packed, 32);
high128 = BigInt(high128);
low128 = BigInt(low128);
const packed = "0x" + ((high128 << 128n) + low128).toString(16);
return hexZeroPad(packed, 32);
}
/**
@@ -196,19 +196,19 @@ export function packUint(high128: BigNumberish, low128: BigNumberish): string {
* in a way the bundler doesn't expect and an error is thrown
*/
export function hexZeroPad(value: BytesLike, length: number): string {
if (typeof value !== "string") {
value = hexlify(value);
} else if (!isHexString(value)) {
console.log("invalid hex string", "value", value);
}
if (typeof value !== "string") {
value = hexlify(value);
} else if (!isHexString(value)) {
console.log("invalid hex string", "value", value);
}
if (value.length > 2 * length + 2) {
console.log("value out of range", "value", arguments[1]);
}
if (value.length > 2 * length + 2) {
console.log("value out of range", "value", arguments[1]);
}
while (value.length < 2 * length + 2) {
value = "0x0" + value.substring(2);
}
while (value.length < 2 * length + 2) {
value = "0x0" + value.substring(2);
}
return value;
return value;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff