Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

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

18 changes: 13 additions & 5 deletions scripts/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ 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";
const OIDC_KEY_REGISTRY_NAME = "OidcKeyRegistry";

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...");
Expand All @@ -26,13 +28,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")
Expand Down Expand Up @@ -76,12 +77,16 @@ 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);
const recovery = await deploy(GUARDIAN_RECOVERY_NAME, deployer, !cmd.noProxy, [webauth, factory], guardianInterface.encodeFunctionData("initialize", [webauth, factory]));
const paymaster = await deploy(PAYMASTER_NAME, deployer, false, [factory, sessions, recovery]);
const oidcKeyRegistryInterface = new ethers.Interface((await hre.artifacts.readArtifact(OIDC_KEY_REGISTRY_NAME)).abi);
await deploy(OIDC_KEY_REGISTRY_NAME, deployer, !cmd.noProxy, [], oidcKeyRegistryInterface.encodeFunctionData("initialize", []));

await fundPaymaster(paymaster, cmd.fund);
} else {
Expand All @@ -105,6 +110,9 @@ task("deploy", "Deploys ZKsync SSO contracts")
}
args = [cmd.factory, cmd.sessions];
}
if (cmd.only == OIDC_KEY_REGISTRY_NAME) {
args = [];
}
const deployedContract = await deploy(cmd.only, deployer, false, args);

if (cmd.only == PAYMASTER_NAME) {
Expand Down
120 changes: 117 additions & 3 deletions src/AAFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pragma solidity ^0.8.24;
import { DEPLOYER_SYSTEM_CONTRACT } from "@matterlabs/zksync-contracts/l2/system-contracts/Constants.sol";
import { IContractDeployer } from "@matterlabs/zksync-contracts/l2/system-contracts/interfaces/IContractDeployer.sol";
import { SystemContractsCaller } from "@matterlabs/zksync-contracts/l2/system-contracts/libraries/SystemContractsCaller.sol";
import { Strings } from "@openzeppelin/contracts/utils/Strings.sol";

import { ISsoAccount } from "./interfaces/ISsoAccount.sol";

Expand All @@ -12,6 +13,8 @@ import { ISsoAccount } from "./interfaces/ISsoAccount.sol";
/// @custom:security-contact security@matterlabs.dev
/// @dev This contract is used to deploy SSO accounts as beacon proxies.
contract AAFactory {
using Strings for string;

/// @notice Emitted when a new account is successfully created.
/// @param accountAddress The address of the newly created account.
/// @param uniqueAccountId A unique identifier for the account.
Expand All @@ -24,6 +27,17 @@ contract AAFactory {
/// @notice A mapping from unique account IDs to their corresponding deployed account addresses.
mapping(string => address) public accountMappings;

/// @notice A mapping from account addresses to their corresponding unique account IDs.
mapping(address => string) public accountIds;

/// @notice A mapping that marks account IDs as being used for recovery.
/// @dev This is used to prevent the same account ID from being used for recovery, deployment and future uses.
mapping(string => address) public recoveryAccountIds;

error AccountAlreadyRegistered(string uniqueAccountId, address accountAddress);
error AccountNotRegistered(string uniqueAccountId, address accountAddress);
error AccountUsedForRecovery(string uniqueAccountId, address accountAddress);

/// @notice Constructor that initializes the factory with a beacon proxy bytecode hash and implementation contract address.
/// @param _beaconProxyBytecodeHash The bytecode hash of the beacon proxy.
/// @param _beacon The address of the UpgradeableBeacon contract used for the SSO accounts' beacon proxies.
Expand All @@ -49,8 +63,6 @@ contract AAFactory {
bytes[] calldata _initialValidators,
address[] calldata _initialK1Owners
) external returns (address accountAddress) {
require(accountMappings[_uniqueAccountId] == address(0), "Account already exists");

(bool success, bytes memory returnData) = SystemContractsCaller.systemCallWithReturndata(
uint32(gasleft()),
address(DEPLOYER_SYSTEM_CONTRACT),
Expand All @@ -63,11 +75,113 @@ contract AAFactory {
require(success, "Deployment failed");
(accountAddress) = abi.decode(returnData, (address));

accountMappings[_uniqueAccountId] = accountAddress;
// Check if the account is already registered
// Note: this check is done at this point, to use `accountAddress` to process the error message.
require(
accountMappings[_uniqueAccountId] == address(0),
AccountAlreadyRegistered(_uniqueAccountId, accountAddress)
);
require(accountIds[accountAddress].equal(""), AccountAlreadyRegistered(_uniqueAccountId, accountAddress));
require(
recoveryAccountIds[_uniqueAccountId] == address(0),
AccountUsedForRecovery(_uniqueAccountId, accountAddress)
);

// Initialize the newly deployed account with validators, hooks and K1 owners.
ISsoAccount(accountAddress).initialize(_initialValidators, _initialK1Owners);

_registerAccount(_uniqueAccountId, accountAddress);

emit AccountCreated(accountAddress, _uniqueAccountId);
}

/// @notice Registers an account with a given account ID.
/// @dev Can only be called by the account's validators.
/// @param _uniqueAccountId The unique identifier for the account.
/// @param _accountAddress The address of the account to register.
function registerAccount(
string calldata _uniqueAccountId,
address _accountAddress
) external onlyAccountValidator(_accountAddress) {
require(
accountMappings[_uniqueAccountId] == address(0),
AccountAlreadyRegistered(_uniqueAccountId, _accountAddress)
);
require(accountIds[_accountAddress].equal(""), AccountAlreadyRegistered(_uniqueAccountId, _accountAddress));
require(
recoveryAccountIds[_uniqueAccountId] == address(0),
AccountUsedForRecovery(_uniqueAccountId, _accountAddress)
);

_registerAccount(_uniqueAccountId, _accountAddress);
}

function _registerAccount(string calldata _uniqueAccountId, address _accountAddress) internal {
accountMappings[_uniqueAccountId] = _accountAddress;
accountIds[_accountAddress] = _uniqueAccountId;
}

/// @notice Unregisters an account from the factory.
/// @dev Can only be called by the account's validators.
/// @param _uniqueAccountId The unique identifier for the account.
/// @param _accountAddress The address of the account to unregister.
function unregisterAccount(
string memory _uniqueAccountId,
address _accountAddress
) external onlyAccountValidator(_accountAddress) {
require(
accountMappings[_uniqueAccountId] == _accountAddress,
AccountNotRegistered(_uniqueAccountId, _accountAddress)
);
require(
accountIds[_accountAddress].equal(_uniqueAccountId),
AccountNotRegistered(_uniqueAccountId, _accountAddress)
);

accountMappings[_uniqueAccountId] = address(0);
accountIds[_accountAddress] = "";
}

/// @notice Updates the account mapping for a given account ID during recovery.
/// @dev Can only be called by the account's validators.
/// @param _uniqueAccountId The unique identifier for the account.
/// @param _accountAddress The address of the account to update the mapping for.
function registerRecoveryBlockedAccount(
string calldata _uniqueAccountId,
address _accountAddress
) external onlyAccountValidator(_accountAddress) {
require(
accountMappings[_uniqueAccountId] == address(0),
AccountAlreadyRegistered(_uniqueAccountId, _accountAddress)
);
require(
recoveryAccountIds[_uniqueAccountId] == address(0),
AccountUsedForRecovery(_uniqueAccountId, _accountAddress)
);

recoveryAccountIds[_uniqueAccountId] = _accountAddress;
}

/// @notice Unregisters a recovery blocked account from the factory.
/// @dev Can only be called by the account's validators.
/// @param _uniqueAccountId The unique identifier for the account.
/// @param _accountAddress The address of the account to unregister.
function unregisterRecoveryBlockedAccount(
string calldata _uniqueAccountId,
address _accountAddress
) external onlyAccountValidator(_accountAddress) {
require(
recoveryAccountIds[_uniqueAccountId] == _accountAddress,
AccountNotRegistered(_uniqueAccountId, _accountAddress)
);

recoveryAccountIds[_uniqueAccountId] = address(0);
}

/// @notice Modifier that checks if the caller is a validator for the given account.
/// @param _accountAddress The address of the account to check the validator for.
modifier onlyAccountValidator(address _accountAddress) {
require(ISsoAccount(_accountAddress).isModuleValidator(msg.sender), "Unauthorized validator");
_;
}
}
55 changes: 55 additions & 0 deletions src/OidcKeyRegistry.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.24;

import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";

contract OidcKeyRegistry is Initializable, OwnableUpgradeable {
uint8 public constant MAX_KEYS = 5;

struct Key {
bytes32 kid; // Key ID
bytes n; // RSA modulus
bytes e; // RSA exponent
}

// Mapping uses keccak256(iss) as the key
mapping(bytes32 => Key[MAX_KEYS]) public OIDCKeys; // Stores up to MAX_KEYS per issuer
mapping(bytes32 => uint8) public keyIndexes; // Tracks the latest key index for each issuer

constructor() {
initialize();
}

function initialize() public initializer {
__Ownable_init();
}

function hashIssuer(string memory iss) public pure returns (bytes32) {
return keccak256(abi.encodePacked(iss));
}

function setKey(bytes32 issHash, Key memory key) public onlyOwner {
uint8 index = keyIndexes[issHash];
uint8 nextIndex = (index + 1) % MAX_KEYS; // Circular buffer
OIDCKeys[issHash][nextIndex] = key;
keyIndexes[issHash] = nextIndex;
}

function setKeys(bytes32 issHash, Key[] memory keys) public onlyOwner {
for (uint8 i = 0; i < keys.length; i++) {
setKey(issHash, keys[i]);
}
}

function getKey(bytes32 issHash, bytes32 kid) public view returns (Key memory) {
require(kid != 0, "Invalid kid");
Key[MAX_KEYS] storage keys = OIDCKeys[issHash];
for (uint8 i = 0; i < MAX_KEYS; i++) {
if (keys[i].kid == kid) {
return keys[i];
}
}
revert("Key not found");
}
}
5 changes: 4 additions & 1 deletion src/TransparentProxy.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
27 changes: 27 additions & 0 deletions src/interfaces/IGuardianRecoveryValidator.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

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, string memory accountId) external;

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

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

function validateSignature(bytes32 signedHash, bytes memory signature) external view returns (bool);
}
23 changes: 21 additions & 2 deletions src/test/ExampleAuthServerPaymaster.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,33 @@ import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";

import { AAFactory } from "../AAFactory.sol";
import { SessionKeyValidator } from "../validators/SessionKeyValidator.sol";
import { GuardianRecoveryValidator } from "../validators/GuardianRecoveryValidator.sol";

/// @author Matter Labs
/// @notice This contract does not include any validations other than using the paymaster general flow.
contract ExampleAuthServerPaymaster is IPaymaster, Ownable {
address public immutable AA_FACTORY_CONTRACT_ADDRESS;
address public immutable SESSION_KEY_VALIDATOR_CONTRACT_ADDRESS;
address public immutable ACCOUNT_RECOVERY_VALIDATOR_CONTRACT_ADDRESS;
bytes4 constant DEPLOY_ACCOUNT_SELECTOR = AAFactory.deployProxySsoAccount.selector;
bytes4 constant SESSION_CREATE_SELECTOR = SessionKeyValidator.createSession.selector;
bytes4 constant SESSION_REVOKE_KEY_SELECTOR = SessionKeyValidator.revokeKey.selector;
bytes4 constant SESSION_REVOKE_KEYS_SELECTOR = SessionKeyValidator.revokeKeys.selector;
bytes4 constant GUARDIAN_RECOVERY_ADD_KEY_SELECTOR = GuardianRecoveryValidator.addValidationKey.selector;
bytes4 constant GUARDIAN_RECOVERY_PROPOSE_KEY_SELECTOR = GuardianRecoveryValidator.proposeValidationKey.selector;
bytes4 constant GUARDIAN_RECOVERY_DISCARD_RECOVERY_SELECTOR = GuardianRecoveryValidator.discardRecovery.selector;
bytes4 constant GUARDIAN_RECOVERY_REMOVE_KEY_SELECTOR = GuardianRecoveryValidator.removeValidationKey.selector;

modifier onlyBootloader() {
require(msg.sender == BOOTLOADER_FORMAL_ADDRESS, "Only bootloader can call this method");
// Continue execution if called from the bootloader.
_;
}

constructor(address aaFactoryAddress, address sessionKeyValidatorAddress) {
constructor(address aaFactoryAddress, address sessionKeyValidatorAddress, address accountRecoveryValidatorAddress) {
AA_FACTORY_CONTRACT_ADDRESS = aaFactoryAddress;
SESSION_KEY_VALIDATOR_CONTRACT_ADDRESS = sessionKeyValidatorAddress;
ACCOUNT_RECOVERY_VALIDATOR_CONTRACT_ADDRESS = accountRecoveryValidatorAddress;
}

function validateAndPayForPaymasterTransaction(
Expand All @@ -44,7 +51,9 @@ contract ExampleAuthServerPaymaster is IPaymaster, Ownable {
// Ensure the transaction is calling one of our allowed contracts
address to = address(uint160(_transaction.to));
require(
to == AA_FACTORY_CONTRACT_ADDRESS || to == SESSION_KEY_VALIDATOR_CONTRACT_ADDRESS,
to == AA_FACTORY_CONTRACT_ADDRESS ||
to == SESSION_KEY_VALIDATOR_CONTRACT_ADDRESS ||
to == ACCOUNT_RECOVERY_VALIDATOR_CONTRACT_ADDRESS,
"Unsupported contract address"
);

Expand All @@ -63,6 +72,16 @@ contract ExampleAuthServerPaymaster is IPaymaster, Ownable {
);
}

if (to == ACCOUNT_RECOVERY_VALIDATOR_CONTRACT_ADDRESS) {
require(
methodSelector == GUARDIAN_RECOVERY_ADD_KEY_SELECTOR ||
methodSelector == GUARDIAN_RECOVERY_PROPOSE_KEY_SELECTOR ||
methodSelector == GUARDIAN_RECOVERY_DISCARD_RECOVERY_SELECTOR ||
methodSelector == GUARDIAN_RECOVERY_REMOVE_KEY_SELECTOR,
"Unsupported method"
);
}

bytes4 paymasterInputSelector = bytes4(_transaction.paymasterInput[0:4]);
require(paymasterInputSelector == IPaymasterFlow.general.selector, "Unsupported paymaster flow");

Expand Down
Loading
Loading