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 17 commits
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
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.

10 changes: 6 additions & 4 deletions scripts/deploy.ts
Original file line number Diff line number Diff line change
@@ -6,12 +6,13 @@ import { Wallet } from "zksync-ethers";

const WEBAUTH_NAME = "WebAuthValidator";
const SESSIONS_NAME = "SessionKeyValidator";
const GUARDIAN_RECOVERY_NAME = "GuardianRecoveryValidator";
const ACCOUNT_IMPL_NAME = "SsoAccount";
const FACTORY_NAME = "AAFactory";
const PAYMASTER_NAME = "ExampleAuthServerPaymaster";
const BEACON_NAME = "SsoBeacon";

async function deploy(name: string, deployer: Wallet, proxy: boolean, args?: any[]): Promise<string> {
async function deploy(name: string, deployer: Wallet, proxy: boolean, args?: any[], initArgs?: any): Promise<string> {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { deployFactory, create2, ethersStaticSalt } = require("../test/utils");
console.log("Deploying", name, "contract...");
@@ -26,13 +27,12 @@ async function deploy(name: string, deployer: Wallet, proxy: boolean, args?: any
console.log(name, "contract deployed at:", implAddress, "\n");
return implAddress;
}
const proxyContract = await create2("TransparentProxy", deployer, ethersStaticSalt, [implAddress]);
const proxyContract = await create2("TransparentProxy", deployer, ethersStaticSalt, [implAddress, initArgs ?? "0x"]);
const proxyAddress = await proxyContract.getAddress();
console.log(name, "proxy contract deployed at:", proxyAddress, "\n");
return proxyAddress;
}


task("deploy", "Deploys ZKsync SSO contracts")
.addOptionalParam("only", "name of a specific contract to deploy")
.addFlag("noProxy", "do not deploy transparent proxies for factory and modules")
@@ -76,12 +76,14 @@ task("deploy", "Deploys ZKsync SSO contracts")
}

if (!cmd.only) {
await deploy(WEBAUTH_NAME, deployer, !cmd.noProxy);
const webauth = await deploy(WEBAUTH_NAME, deployer, !cmd.noProxy);
const sessions = await deploy(SESSIONS_NAME, deployer, !cmd.noProxy);
const implementation = await deploy(ACCOUNT_IMPL_NAME, deployer, false);
const beacon = await deploy(BEACON_NAME, deployer, false, [implementation]);
const factory = await deploy(FACTORY_NAME, deployer, !cmd.noProxy, [beacon]);
const paymaster = await deploy(PAYMASTER_NAME, deployer, false, [factory, sessions]);
const guardianInterface = new ethers.Interface((await hre.artifacts.readArtifact(GUARDIAN_RECOVERY_NAME)).abi);
await deploy(GUARDIAN_RECOVERY_NAME, deployer, !cmd.noProxy, [webauth], guardianInterface.encodeFunctionData("initialize", [webauth]));

await fundPaymaster(paymaster, cmd.fund);
} else {
5 changes: 4 additions & 1 deletion src/TransparentProxy.sol
Original file line number Diff line number Diff line change
@@ -12,7 +12,10 @@ import { TransparentUpgradeableProxy } from "@openzeppelin/contracts/proxy/trans
/// cheap delegate calls on ZKsync.
/// @dev This proxy is placed in front of `AAFactory` and all modules (`WebAuthValidator`, `SessionKeyValidator`).
contract TransparentProxy is TransparentUpgradeableProxy, EfficientProxy {
constructor(address implementation) TransparentUpgradeableProxy(implementation, msg.sender, bytes("")) {}
constructor(
address implementation,
bytes memory data
) TransparentUpgradeableProxy(implementation, msg.sender, data) {}

function _delegate(address implementation) internal override(EfficientProxy, Proxy) {
EfficientProxy._delegate(implementation);
28 changes: 28 additions & 0 deletions src/interfaces/IGuardianRecoveryValidator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// SPDX-License-Identifier: MIT
import { IModuleValidator } from "./IModuleValidator.sol";
import { Transaction } from "@matterlabs/zksync-contracts/l2/system-contracts/libraries/TransactionHelper.sol";

interface IGuardianRecoveryValidator is IModuleValidator {
struct GuardianConfirmation {
address ssoAccount;
}

function proposeValidationKey(address externalAccount) external;

function removeValidationKey(address externalAccount) external;

function initRecovery(address accountToRecover, bytes memory passkey) external;

// IModuleValidator
function addValidationKey(bytes memory key) external returns (bool);

// IModuleValidator
function validateTransaction(
bytes32 signedHash,
bytes memory signature,
Transaction calldata transaction
) external returns (bool);

// IModuleValidator
function validateSignature(bytes32 signedHash, bytes memory signature) external view returns (bool);
}
228 changes: 228 additions & 0 deletions src/validators/GuardianRecoveryValidator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
// 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 { Initializable } from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import { IModuleValidator } from "../interfaces/IModuleValidator.sol";
import { SignatureDecoder } from "../libraries/SignatureDecoder.sol";

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

error GuardianNotFound(address guardian);
error GuardianNotProposed(address guardian);
error PasskeyNotMatched();
error CooldownPerionNotPassed();
error ExpiredRequest();

/**
* @dev Event indicating new recovery process being initiated
*/
event RecoveryInitiated(address account, address guardian);

uint256 constant REQUEST_VALIDITY_TIME = 72 * 60 * 60; // 72 hours
uint256 constant REQUEST_DELAY_TIME = 24 * 60 * 60; // 24 hours

mapping(address account => Guardian[]) public accountGuardians;
mapping(address account => RecoveryRequest) public pendingRecoveryData;

address public webAuthValidator;

/**
* @notice The constructor sets the web authn validator for which recovery process can be initiated. Used only for non proxied deployment
*/
constructor(address _webAuthValidator) {
initialize(_webAuthValidator);
}

/// @notice Initializer function that sets validator initial configuration. Expected to be used in the proxy.
/// @dev Sets webAuthValidator address
/// @param _webAuthValidator Address of WebAuthnValidator contracts
function initialize(address _webAuthValidator) public initializer {
webAuthValidator = _webAuthValidator;
}

/**
* @notice Validator initiator for given sso account. This module does not support initialization on creation
* @param initData Not used
*/
function init(bytes calldata initData) external {}

/**
* @notice Removes all past guardians when this module is disabled in a account
*/
function disable() external {
delete accountGuardians[msg.sender];
}

/**
* @notice The `proposeValidationKey` method handles the initial registration of external accounts by:
* 1. Taking an external account address and store it as pending guardian
* 2. Enable `addValidationKey` to confirm this account
* @param newGuardian New Guardian's address
*/
function proposeValidationKey(address newGuardian) external {
Guardian[] storage guardians = accountGuardians[msg.sender];

// If the guardian exist this method stops
for (uint256 i = 0; i < guardians.length; i++) {
if (guardians[i].addr == newGuardian) {
return;
}
}

guardians.push(Guardian(newGuardian, false));
}

/**
* @notice This method handles the removal of external accounts by:
* 1. Accepting an address as input
* 2. Removing the account from the list of guardians
* @param guardianToRemove Guardian's address to remove
*/
function removeValidationKey(address guardianToRemove) external {
Guardian[] storage guardians = accountGuardians[msg.sender];

// Searchs guardian with given address
for (uint256 i = 0; i < guardians.length; i++) {
if (guardians[i].addr == guardianToRemove) {
// If found last guardian is moved to current position, and then
// last element is removed from array.
guardians[i] = guardians[guardians.length - 1];
guardians.pop();
return;
}
}

revert GuardianNotFound(guardianToRemove);
}

/**
* @notice This method allows to accept being a guardian of given account
* @param key Encoded address of account which msg.sender is becoming guardian of
* @return Flag indicating whether guardian was already valid or not
*/
function addValidationKey(bytes memory key) external returns (bool) {
// Interprets argument as address;
address accountToGuard = abi.decode(key, (address));
Guardian[] storage guardians = accountGuardians[accountToGuard];

// Searches if the caller is in the list of guardians.
// If guardian found is set to true.
for (uint256 i = 0; i < guardians.length; i++) {
if (guardians[i].addr == msg.sender) {
// We return true if the guardian was not confirmed before.
bool retValue = !guardians[i].isReady;
guardians[i].isReady = true;
return retValue;
}
}

revert GuardianNotProposed(msg.sender);
}

/**
* @notice This modifier allows execution only by active guardian of account
* @param account Address of account for which we verify guardian existence
*/
modifier onlyGuardianOf(address account) {
bool isGuardian = false;
for (uint256 i = 0; i < accountGuardians[account].length; i++) {
if (accountGuardians[account][i].addr == msg.sender && accountGuardians[account][i].isReady) isGuardian = true;
break;
}
if (!isGuardian) revert GuardianNotFound(msg.sender);
// Continue execution if called by guardian
_;
}

/**
* @notice This method allows to accept being a guardian of given account
* @param accountToRecover Address of account for which given recovery is initiated
* @param passkey Encoded new passkey, that will be passed to WebAuthnModule
*/
function initRecovery(address accountToRecover, bytes memory passkey) external onlyGuardianOf(accountToRecover) {
pendingRecoveryData[accountToRecover] = RecoveryRequest(passkey, block.timestamp);

emit RecoveryInitiated(accountToRecover, msg.sender);
}

/**
* @notice This method allows to discard currently pending recovery
*/
function discardRecovery() external {
delete pendingRecoveryData[msg.sender];
}

/**
* @inheritdoc IModuleValidator
*/
function validateTransaction(
bytes32 signedHash,
bytes memory signature,
Transaction calldata transaction
) external returns (bool) {
// If the user has a recovery in progress then:
// 1. The method will verify calls to `WebAuthnModule`
// 2. Checks if the transaction is attempting to modify passkeys
// 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(webAuthValidator)) {
// Check for calling "addValidationKey" method by anyone on WebAuthValidator contract
require(selector == WebAuthValidator.addValidationKey.selector, "Unsupported function call");
bytes memory validationKeyData = abi.decode(transaction.data[4:], (bytes));

// Verify that current request matches pending one
if (
pendingRecoveryData[msg.sender].passkey.length != validationKeyData.length ||
keccak256(pendingRecoveryData[msg.sender].passkey) != keccak256(validationKeyData)
) revert PasskeyNotMatched();

// Ensure time constraints
uint256 timePassedSinceRequest = block.timestamp - pendingRecoveryData[msg.sender].timestamp;
if (timePassedSinceRequest < REQUEST_DELAY_TIME) revert CooldownPerionNotPassed();
if (timePassedSinceRequest > REQUEST_VALIDITY_TIME) revert ExpiredRequest();

// Cleanup currently processed recovery data
delete pendingRecoveryData[msg.sender];

return true;
}

return false;
}

// This module is not meant to be used to validate signatures
function validateSignature(bytes32 signedHash, bytes memory signature) external view returns (bool) {
return false;
}

function supportsInterface(bytes4 interfaceId) external view returns (bool) {
return interfaceId == type(IERC165).interfaceId || interfaceId == type(IModuleValidator).interfaceId;
}

function guardiansFor(address addr) public view returns (Guardian[] memory) {
return accountGuardians[addr];
}
}
Loading