Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Draft] feat: add guardian recovery #261

Draft
wants to merge 42 commits into
base: main
Choose a base branch
from
Draft
Changes from 1 commit
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
e9c2b5e
feat: empty GuardianRecoveryValidator
calvogenerico Jan 9, 2025
df852cf
feat: methods to add a guardian
calvogenerico Jan 9, 2025
3972ec2
fix: reverting when guardian not found
calvogenerico Jan 9, 2025
1a6def0
fix: uint to uint256
calvogenerico Jan 9, 2025
b1478cd
feat: add validateTransaction implementation to GuardianRecoveryValid…
MiniRoman Jan 17, 2025
f01162c
chore: refactor tests
MiniRoman Jan 17, 2025
a7eec81
chore: clean up code
MiniRoman Jan 17, 2025
3290826
feat: improve init method
MiniRoman Jan 17, 2025
fde86e3
feat: simplify initRecovery method
MiniRoman Jan 22, 2025
731ffd7
chore: resolve build issues
MiniRoman Jan 22, 2025
d9dc82b
chore: resolve build issues
MiniRoman Jan 22, 2025
94ffc8b
chore: resolve pr comments
MiniRoman Jan 23, 2025
771c586
feat: restore guardiansFor method
MiniRoman Jan 23, 2025
b36bcb2
chore: remove unused access to accountGuardians
MiniRoman Jan 23, 2025
24f34e8
feat: make guardian recovery validator contract proxy-able
MiniRoman Jan 23, 2025
4c73093
chore: simplify initializer function name
MiniRoman Jan 24, 2025
4ae13ba
Merge pull request #1 from Moonsong-Labs/feat/guardian-module
aon Jan 24, 2025
064764b
feat: add function to retrieve guarded accounts
MiniRoman Jan 24, 2025
afb9c70
fix: improve recovery validator logic
aon Jan 24, 2025
a09d7e2
Merge pull request #2 from Moonsong-Labs/feat/guardian-module
aon Jan 24, 2025
460446c
feat: allow paymaster calls to GuardianRecoveryValidator
MiniRoman Jan 28, 2025
b408afc
Merge pull request #3 from Moonsong-Labs/feat/guardian-module
aon Jan 28, 2025
ddac18b
feat: merge from working branch
aon Jan 30, 2025
0ec4f89
feat: fix guardian recovery validator compilation
MiniRoman Jan 30, 2025
59cff84
fix: add compiler version and remove unwanted comments
aon Jan 30, 2025
b5e95c6
fix: bugs and jsdoc format to match rest of package
aon Jan 30, 2025
5f4feea
fix: test that included guardian contract
aon Jan 30, 2025
a759787
feat: add passkey to account relation
aon Jan 31, 2025
303827d
feat: prevent account overlap
aon Jan 31, 2025
9768a19
feat: improve registered accounts logic
aon Feb 3, 2025
2d4d3f1
fix: tests
aon Feb 3, 2025
e14f484
fix: unknown accounts
aon Feb 3, 2025
b147d63
fix: discard recovery bug
aon Feb 3, 2025
62eb5fe
fix: move account verifications
aon Feb 3, 2025
cae4e89
feat: add guardian added time to guardian information
MiniRoman Jan 31, 2025
ae25098
fix: deployment
aon Feb 3, 2025
b8fe05b
fix: address to account id is not empty when initiating recovery
MiniRoman Feb 4, 2025
ac2d360
fix: remove double save on guardedAccounts
aon Feb 4, 2025
b41bed1
Add OidcKeyRegistry
matias-gonz Feb 6, 2025
ecde01c
Update deploy script
matias-gonz Feb 6, 2025
fb48235
Fix/paymaster-recovery-validator (#291)
aon Feb 13, 2025
c248dae
Merge branch 'feat/oidc-account-recovery' into guardian-recovery
matias-gonz Feb 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat: add validateTransaction implementation to GuardianRecoveryValid…
…atior
MiniRoman committed Jan 17, 2025
commit b1478cd83d5194ed4747d3755c4b90f83e529b15
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -37,6 +37,7 @@
"@nomad-xyz/excessively-safe-call": "^0.0.1-rc.1",
"@nomicfoundation/hardhat-chai-matchers": "2.0.8",
"@nomicfoundation/hardhat-ethers": "3.0.8",
"@nomicfoundation/hardhat-network-helpers": "^1.0.12",
"@nomicfoundation/hardhat-toolbox": "^5.0.0",
"@nomicfoundation/hardhat-verify": "2.0.11",
"@openzeppelin/contracts": "4.9.6",
19 changes: 18 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

83 changes: 78 additions & 5 deletions src/validators/GuardianRecoveryValidator.sol
Original file line number Diff line number Diff line change
@@ -1,18 +1,49 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import { IGuardianRecoveryValidator } from "../interfaces/IGuardianRecoveryValidator.sol";
import { WebAuthValidator } from "./WebAuthValidator.sol";
import { Transaction } from "@matterlabs/zksync-contracts/l2/system-contracts/libraries/TransactionHelper.sol";
import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import { IERC165 } from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
import { IModuleValidator } from "../interfaces/IModuleValidator.sol";
import { SignatureDecoder } from "../libraries/SignatureDecoder.sol";

contract GuardianRecoveryValidator is IGuardianRecoveryValidator {
struct Guardian {
address addr;
bool isReady;
}
struct RecoveryRequest {
bytes passkey;
uint256 timestamp;
}

error GuardianNotFound(address guardian);
error GuardianNotProposed(address guardian);

event RecoveryInitiated();

mapping(address account => Guardian[]) accountGuardians;
uint256 constant REQUEST_VALIDITY_TIME = 72 * 60 * 60; // 72 hours
uint256 constant REQUEST_DELAY_TIME = 24 * 60 * 60; // 24 hours

function init(bytes calldata initData) external {}
mapping(address account => Guardian[]) public accountGuardians;
mapping(address account => mapping(address validator => RecoveryRequest)) public pendingRecoveryData;

address public webAuthValidator;

constructor(address _webAuthValidator) {
webAuthValidator = _webAuthValidator;
}

function init(bytes calldata initData) external {
address[] memory initialGuardians = abi.decode(initData, (address[]));
Guardian[] storage guardians = accountGuardians[msg.sender];

for (uint256 i = 0; i < initialGuardians.length; i++) {
guardians.push(Guardian(initialGuardians[i], true)); // Make initial guardians active instanenously
}
}

// When this module is disabled in an account all the
// data associated with that account is freed.
@@ -55,7 +86,7 @@ contract GuardianRecoveryValidator is IGuardianRecoveryValidator {
}
}

revert("Guardian not found.");
revert GuardianNotFound(guardianToRemove);
}

// IModuleValidator
@@ -76,12 +107,15 @@ contract GuardianRecoveryValidator is IGuardianRecoveryValidator {
}
}

revert("Guardian was not proposed for given account.");
revert GuardianNotProposed(msg.sender);
}

// This method has to start the recovery.
// It's called by the sso account.
function initRecovery(bytes memory passkey) external {}
function initRecovery(bytes memory passkey) external {
pendingRecoveryData[msg.sender][webAuthValidator] = RecoveryRequest(passkey, block.timestamp);
emit RecoveryInitiated();
}

// IModuleValidator
function validateTransaction(
@@ -99,6 +133,45 @@ contract GuardianRecoveryValidator is IGuardianRecoveryValidator {
// 3. Verify the new passkey is the one stored in `initRecovery`
// 4. Allows anyone to call this method, as the recovery was already verified in `initRecovery`
// 5. Verifies that the required timelock period has passed since `initRecovery` was called
(bytes memory transactionSignature, address _validator, bytes memory validatorData) = SignatureDecoder
.decodeSignature(transaction.signature);

require(transaction.data.length >= 4, "Only function calls are supported");
bytes4 selector = bytes4(transaction.data[:4]);

require(transaction.to <= type(uint160).max, "Overflow");
address target = address(uint160(transaction.to));

if (target == address(this)) {
require(selector == this.initRecovery.selector, "Unsupported function call");

(address recoveredAddress, ECDSA.RecoverError recoverError) = ECDSA.tryRecover(signedHash, transactionSignature);
if (recoverError != ECDSA.RecoverError.NoError || recoveredAddress == address(0)) {
return false;
}
for (uint256 i = 0; i < accountGuardians[msg.sender].length; i++) {
if (accountGuardians[msg.sender][i].addr == recoveredAddress && accountGuardians[msg.sender][i].isReady)
return true;
}
} else if (target == address(webAuthValidator)) {
require(selector == WebAuthValidator.addValidationKey.selector, "Unsupported function call");
bytes memory validationKeyData = abi.decode(transaction.data[4:], (bytes));

require(
pendingRecoveryData[msg.sender][target].passkey.length == validationKeyData.length &&
keccak256(pendingRecoveryData[msg.sender][target].passkey) == keccak256(validationKeyData),
"New Passkey not matched with recent request"
);

uint256 timePassedSinceRequest = block.timestamp - pendingRecoveryData[msg.sender][target].timestamp;
require(timePassedSinceRequest > REQUEST_DELAY_TIME, "Cooldown period not passed");
require(timePassedSinceRequest < REQUEST_VALIDITY_TIME, "Request not valid anymore");

delete pendingRecoveryData[msg.sender][target];

return true;
}

return false;
}

301 changes: 274 additions & 27 deletions test/GuardianRecoveryValidatorTest.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,89 @@
import { Address, encodeAbiParameters, Hex, parseEther } from "viem";
import { ContractFixtures, getProvider } from "./utils";
import { cacheBeforeEach, ContractFixtures, getProvider } from "./utils";
import { expect } from "chai";
import { Wallet } from "zksync-ethers";
import { GuardianRecoveryValidator, GuardianRecoveryValidator__factory } from "../typechain-types";
import { HDNodeWallet } from "ethers";
import { Provider, SmartAccount, utils, Wallet } from "zksync-ethers";
import { AAFactory, GuardianRecoveryValidator, GuardianRecoveryValidator__factory, SsoAccount, SsoAccount__factory, WebAuthValidator } from "../typechain-types";
import { ethers, HDNodeWallet } from "ethers";
import * as helpers from "@nomicfoundation/hardhat-network-helpers";
import { encodeKeyFromBytes, generateES256R1Key, getRawPublicKeyFromCrpyto } from "./PasskeyModule";
import { randomBytes } from "crypto";

describe("GuardianRecoveryValidator", function () {
const fixtures = new ContractFixtures();
const abiCoder = new ethers.AbiCoder();
const provider = getProvider();
let guardiansValidatorAddr: Address;
let factory: AAFactory;
let ssoAccountInstance: SsoAccount;
let newGuardianConnectedSsoAccount: SmartAccount;
let ownerConnectedSsoAccount: SmartAccount;
let guardianWallet: Wallet;
let ownerWallet: Wallet;
let webauthn: WebAuthValidator;
let guardianValidator: GuardianRecoveryValidator;


this.beforeEach(async () => {
const guardiansValidator = await fixtures.getGuardianRecoveryValidator();
guardiansValidatorAddr = await guardiansValidator.getAddress() as Address;
cacheBeforeEach(async () => {
guardianWallet = new Wallet(Wallet.createRandom().privateKey, provider);
ownerWallet = new Wallet(Wallet.createRandom().privateKey, provider);
console.log("guardianWallet")

console.log(guardianWallet.address)

const generatedKey = await generatePassKey();

guardianValidator = await fixtures.getGuardianRecoveryValidator();
webauthn = await fixtures.getWebAuthnVerifierContract();
guardiansValidatorAddr = await guardianValidator.getAddress() as Address;
factory = await fixtures.getAaFactory()
const randomSalt = randomBytes(32);
const accountId = "session-key-test-id" + randomBytes(32).toString();
const initialValidators = [
ethers.AbiCoder.defaultAbiCoder().encode(['address', 'bytes'], [await webauthn.getAddress(), generatedKey]),
ethers.AbiCoder.defaultAbiCoder().encode(['address', 'bytes'], [await guardianValidator.getAddress(), ethers.AbiCoder.defaultAbiCoder().encode(
['address[]'],
[[]]
)])
];
ssoAccountInstance = SsoAccount__factory.connect(await factory.deployProxySsoAccount.staticCall(
randomSalt,
accountId,
initialValidators,
[ownerWallet]
), fixtures.wallet)
await factory.deployProxySsoAccount(
randomSalt,
accountId,
initialValidators,
[ownerWallet]
)
const ssoAccountInstanceAddress = await ssoAccountInstance.getAddress();
console.log("ssoAccountInstanceAddress")
console.log(ssoAccountInstanceAddress)
const fundTx = await fixtures.wallet.sendTransaction({ value: parseEther("0.2"), to: ssoAccountInstanceAddress });
const fundTx2 = await (await fixtures.wallet.sendTransaction({ value: parseEther("0.2"), to: guardianWallet.address })).wait();
newGuardianConnectedSsoAccount = new SmartAccount({
payloadSigner: async (hash) => {
const data = abiCoder.encode(
["bytes", "address", "bytes"],
[
guardianWallet.signingKey.sign(hash).serialized,
guardiansValidatorAddr,
abiCoder.encode(
['uint256'],
[123]
),
],
);
return data
},
address: await ssoAccountInstance.getAddress(),
secret: guardianWallet.privateKey,
}, provider);
ownerConnectedSsoAccount = new SmartAccount({
address: await ssoAccountInstance.getAddress(),
secret: ownerWallet.privateKey,
}, provider);
})

async function randomWallet(): Promise<[HDNodeWallet, GuardianRecoveryValidator]> {
@@ -23,16 +94,15 @@ describe("GuardianRecoveryValidator", function () {
return [wallet, connected]
}

async function callAddValidationKey(contract: GuardianRecoveryValidator, account: String): Promise<void> {
function callAddValidationKey(contract: GuardianRecoveryValidator, account: String): Promise<ethers.ContractTransactionResponse> {
const encoded = encodeAbiParameters(
[{ type: "address" }],
[account as Hex]
)
const tx = await contract.addValidationKey(encoded);
await tx.wait();
return contract.addValidationKey(encoded);
}

it('can propose a guardian', async function() {
it('can propose a guardian', async function () {
const [user1, connectedUser1] = await randomWallet();
const [guardian] = await randomWallet();

@@ -49,29 +119,21 @@ describe("GuardianRecoveryValidator", function () {
const [user1] = await randomWallet();
const [_guardian, guardianConnection] = await randomWallet();

try {
await callAddValidationKey(guardianConnection, user1.address)
} catch (e) {
return expect(e.shortMessage).to.eql("execution reverted: Guardian was not proposed for given account.")
}
expect.fail("should have reverted")
await expect(callAddValidationKey(guardianConnection, user1.address))
.to.revertedWithCustomError(guardianConnection, "GuardianNotProposed")
})

it("fails when tries to confirm a was proposed for a different account.", async function () {
const [_user1, user1Connection] = await randomWallet();
const [user2] = await randomWallet();
const [guardian, guardianConnection] = await randomWallet();


const tx1 = await user1Connection.proposeValidationKey(guardian.address);
await tx1.wait();

try {
await callAddValidationKey(guardianConnection, user2.address)
} catch (e) {
return expect(e.shortMessage).to.eql("execution reverted: Guardian was not proposed for given account.")
}
expect.fail("should have reverted")
await expect(callAddValidationKey(guardianConnection, user2.address))
.to.revertedWithCustomError(guardianConnection, "GuardianNotProposed");
})

it("works to confirm a proposed account.", async function () {
@@ -82,12 +144,197 @@ describe("GuardianRecoveryValidator", function () {
const tx = await user1Connected.proposeValidationKey(guardian.address);
await tx.wait();


await callAddValidationKey(guardianConnected, user1.address)

const res = await user1Connected.getFunction("guardiansFor").staticCall(user1.address);
expect(res.length).to.equal(1);
expect(res[0][0]).to.equal(guardian.address);
expect(res[0][1]).to.equal(true);
})
})

describe('When attached to SsoAccount', () => {
describe('When initiating new guardian addition operation', () => {
it("it adds guardian as non ready one.", async function () {
const [newGuardianWallet] = await randomWallet();
const functionData = guardianValidator.interface.encodeFunctionData(
'proposeValidationKey',
[newGuardianWallet.address]
);
const txToSign = {
...(await aaTxTemplate(await ssoAccountInstance.getAddress(), provider)),
type: 1,
to: guardiansValidatorAddr,
data: functionData
};
txToSign.gasLimit = await provider.estimateGas(txToSign);
const txData = await ownerConnectedSsoAccount.signTransaction(txToSign)
const tx = await provider.broadcastTransaction(txData);
await tx.wait()

const [newGuardian] = (await guardianValidator.guardiansFor(newGuardianConnectedSsoAccount.address)).slice(-1);
expect(newGuardian.addr).to.eq(newGuardianWallet.address)
expect(newGuardian.isReady).to.be.false;
})
})
describe('When approving existing guardian addition operation', () => {
cacheBeforeEach(async () => {
const functionData = guardianValidator.interface.encodeFunctionData(
'proposeValidationKey',
[guardianWallet.address]
);
const txToSign = {
...(await aaTxTemplate(await ssoAccountInstance.getAddress(), provider)),
to: guardiansValidatorAddr,
data: functionData
};
txToSign.gasLimit = await provider.estimateGas(txToSign);
const txData = await ownerConnectedSsoAccount.signTransaction(txToSign)
const tx = await provider.broadcastTransaction(txData);
await tx.wait()
});
const sut = async () => {
return guardianValidator.connect(guardianWallet)
.addValidationKey(abiCoder.encode(['address'], [newGuardianConnectedSsoAccount.address]));
}
it("it makes guardian active one.", async function () {
await sut();

const [newGuardian] = (await guardianValidator.guardiansFor(newGuardianConnectedSsoAccount.address)).slice(-1);
expect(newGuardian.addr).to.eq(guardianWallet.address)
expect(newGuardian.isReady).to.be.true;
})
})
describe('When having active guardian', () => {
cacheBeforeEach(async () => {
const functionData = guardianValidator.interface.encodeFunctionData(
'proposeValidationKey',
[guardianWallet.address]
);
const txToSign = {
...(await aaTxTemplate(await ssoAccountInstance.getAddress(), provider)),
to: guardiansValidatorAddr,
data: functionData
};
txToSign.gasLimit = await provider.estimateGas(txToSign);
const txData = await ownerConnectedSsoAccount.signTransaction(txToSign)
const tx = await provider.broadcastTransaction(txData);
await tx.wait()
await guardianValidator.connect(guardianWallet).addValidationKey(abiCoder.encode(['address'], [newGuardianConnectedSsoAccount.address]));
});

describe('And initiating recovery process', () => {
let newKey: string;
let refTimestamp: number;

cacheBeforeEach(async () => {
newKey = await generatePassKey();
refTimestamp = (await provider.getBlock('latest')).timestamp + 240;
await helpers.time.setNextBlockTimestamp(refTimestamp)
})
const sut = async () => {

const functionData = guardianValidator.interface.encodeFunctionData(
'initRecovery',
[newKey]
);
const txToSign = {
...(await aaTxTemplate(await ssoAccountInstance.getAddress(), provider)),
to: guardiansValidatorAddr,
data: functionData
};
txToSign.gasLimit = await provider.estimateGas(txToSign);
const txData = await newGuardianConnectedSsoAccount.signTransaction(txToSign)
const tx = await provider.broadcastTransaction(txData);
await tx.wait()
}
it("it creates new recovery process.", async function () {
await sut();

const request = (await guardianValidator.pendingRecoveryData(
newGuardianConnectedSsoAccount.address,
webauthn
));
expect(request.passkey).to.eq(newKey)
expect(request.timestamp).to.eq(refTimestamp);
})
})
describe('And has active recovery process and trying to execute', () => {
let newKey: string;
let refTimestamp: number;

cacheBeforeEach(async () => {
newKey = await generatePassKey();
refTimestamp = (await provider.getBlock('latest')).timestamp + 480;
await helpers.time.setNextBlockTimestamp(refTimestamp)

const functionData = guardianValidator.interface.encodeFunctionData(
'initRecovery',
[newKey]
);
const txToSign = {
...(await aaTxTemplate(await ssoAccountInstance.getAddress(), provider)),
to: guardiansValidatorAddr,
data: functionData
};
txToSign.gasLimit = await provider.estimateGas(txToSign);
const txData = await newGuardianConnectedSsoAccount.signTransaction(txToSign)
const tx = await provider.broadcastTransaction(txData);
await tx.wait()
})
const sut = async () => {
const functionData = webauthn.interface.encodeFunctionData(
'addValidationKey',
[newKey]
);
const txToSign = {
...(await aaTxTemplate(await ssoAccountInstance.getAddress(), provider)),
to: await webauthn.getAddress(),
data: functionData
};
txToSign.gasLimit = await provider.estimateGas(txToSign);
const txData = await newGuardianConnectedSsoAccount.signTransaction(txToSign)
const tx = await provider.broadcastTransaction(txData);
await tx.wait()
}
it("it should clean up pending request.", async function () {

await helpers.time.increase(2*24*60*60)
await sut();

const request = (await guardianValidator.pendingRecoveryData(
newGuardianConnectedSsoAccount.address,
webauthn
));
expect(request.passkey).to.eq('0x')
expect(request.timestamp).to.eq(0);
})
})
})
})
})

async function generatePassKey() {
const keyDomain = randomBytes(32).toString("hex");
const generatedR1Key = await generateES256R1Key();
const [generatedX, generatedY] = await getRawPublicKeyFromCrpyto(generatedR1Key);
const generatedKey = encodeKeyFromBytes([generatedX, generatedY], keyDomain);
return generatedKey;
}

async function aaTxTemplate(proxyAccountAddress: string, provider: Provider) {
return {
type: 113,
from: proxyAccountAddress,
data: "0x",
value: 0,
chainId: (await provider.getNetwork()).chainId,
nonce: await provider.getTransactionCount(proxyAccountAddress),
gasPrice: await provider.getGasPrice(),
customData: {
gasPerPubdata: utils.DEFAULT_GAS_PER_PUBDATA_LIMIT,
customSignature: undefined,
},
gasLimit: 0n,
};
}
6 changes: 3 additions & 3 deletions test/PasskeyModule.ts
Original file line number Diff line number Diff line change
@@ -156,7 +156,7 @@ async function getPublicKey(publicPasskey: Uint8Array): Promise<[Hex, Hex]> {
return [`0x${Buffer.from(x).toString("hex")}`, `0x${Buffer.from(y).toString("hex")}`];
}

async function getRawPublicKeyFromCrpyto(cryptoKeyPair: CryptoKeyPair) {
export async function getRawPublicKeyFromCrpyto(cryptoKeyPair: CryptoKeyPair) {
const keyMaterial = await crypto.subtle.exportKey("raw", cryptoKeyPair.publicKey);
return [new Uint8Array(keyMaterial.slice(1, 33)), new Uint8Array(keyMaterial.slice(33, 65))];
}
@@ -273,7 +273,7 @@ export async function toHash(data: Uint8Array | string): Promise<Uint8Array> {
}

// Generate an ECDSA key pair with the P-256 curve (secp256r1)
async function generateES256R1Key() {
export async function generateES256R1Key() {
return await crypto.subtle.generateKey(r1KeygenParams, false, ["sign", "verify"]);
}

@@ -396,7 +396,7 @@ function encodeKeyFromHex(hexStrings: [Hex, Hex], domain: string) {
)
}

function encodeKeyFromBytes(bytes: [Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>], domain: string) {
export function encodeKeyFromBytes(bytes: [Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>], domain: string) {
return encodeKeyFromHex([toHex(bytes[0]), toHex(bytes[1])], domain);
}

32 changes: 31 additions & 1 deletion test/utils.ts
Original file line number Diff line number Diff line change
@@ -6,11 +6,16 @@ import { ethers, parseEther, randomBytes } from "ethers";
import { readFileSync } from "fs";
import { promises } from "fs";
import * as hre from "hardhat";
import {
SnapshotRestorer,
takeSnapshot,
} from "@nomicfoundation/hardhat-network-helpers";
import { ContractFactory, Provider, utils, Wallet } from "zksync-ethers";
import { base64UrlToUint8Array, getPublicKeyBytesFromPasskeySignature, unwrapEC2Signature } from "zksync-sso/utils";

import { AAFactory, ERC20, ExampleAuthServerPaymaster, SessionKeyValidator, SsoAccount, WebAuthValidator, SsoBeacon, AccountProxy__factory, AccountProxy, GuardianRecoveryValidator, GuardianRecoveryValidator__factory } from "../typechain-types";
import { AAFactory__factory, ERC20__factory, ExampleAuthServerPaymaster__factory, SessionKeyValidator__factory, SsoAccount__factory, WebAuthValidator__factory, SsoBeacon__factory } from "../typechain-types";
import { AsyncFunc } from "mocha";

export const ethersStaticSalt = new Uint8Array([
205, 241, 161, 186, 101, 105, 79,
@@ -76,7 +81,7 @@ export class ContractFixtures {
private _guardianRecoveryValidator: GuardianRecoveryValidator
async getGuardianRecoveryValidator () {
if (this._guardianRecoveryValidator === undefined) {
const contract = await create2("GuardianRecoveryValidator", this.wallet, ethersStaticSalt);
const contract = await create2("GuardianRecoveryValidator", this.wallet, ethersStaticSalt, [await (await this.getWebAuthnVerifierContract()).getAddress()]);
this._guardianRecoveryValidator = GuardianRecoveryValidator__factory.connect(await contract.getAddress(), this.wallet);
}
return this._guardianRecoveryValidator
@@ -387,3 +392,28 @@ export class RecordedResponse {
// the domain linked the passkey that needs to be validated
readonly expectedOrigin: string;
}

const SNAPSHOTS: SnapshotRestorer[] = [];

export function cacheBeforeEach(initializer: AsyncFunc): void {
let initialized = false;

beforeEach(async function () {
if (!initialized) {
await initializer.call(this);
SNAPSHOTS.push(await takeSnapshot());
initialized = true;
} else {
const snapshotId = SNAPSHOTS.pop()!;
await snapshotId.restore();
SNAPSHOTS.push(await takeSnapshot());
}
});

after(async function () {
if (initialized) {
const snapshotId = SNAPSHOTS.pop()!;
await snapshotId.restore();
}
});
}