-
Notifications
You must be signed in to change notification settings - Fork 4
docs: add dev docs #53
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
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
0f26fcd
docs: add dev docs
ly0va aaf9602
expand and fix typos
ly0va 87dc3d4
grammar and typos
ly0va ffddfa8
link docs from readme
ly0va 9d1d5f7
split and expand a bit
ly0va 40822d1
typo
ly0va 9c677c7
Merge branch 'main' into lyova-dev-docs
ly0va File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| # Developer Documentation | ||
|
|
||
| ## Overview | ||
|
|
||
| ZKsync SSO is a modular smart account compliant with [ERC-4337](https://eips.ethereum.org/EIPS/eip-4337) and [ERC-7579](https://eips.ethereum.org/EIPS/eip-7579) and based on the [ERC-7579 reference implementation](https://github.com/erc7579/erc7579-implementation). | ||
|
|
||
| Being familiar with these standards can prove useful while reading this documentation. | ||
|
|
||
| ## Features | ||
|
|
||
| - **Modular & Extendable Architecture**: Pluggable validators and executors following the ERC-7579 standard; supports existing 3rd-party modules | ||
| - **Multiple Authentication Methods**: Support for EOA keys, WebAuthn passkeys, and session keys | ||
| - **Session Key Support**: Grant third parties limited, time-bound access with fine-grained permissions | ||
| - **Account Recovery**: Guardian-based recovery system for lost keys or passkeys | ||
| - **Upgradeable**: Factory and modules are behind transparent proxies; accounts use beacon proxies | ||
|
|
||
| ## Documentation | ||
|
|
||
| - [Architecture](./architecture.md) - System design and component relationships | ||
| - [Deploying](./deploying.md) - Deployment instructions and scripts | ||
| - [Modules](./modules.md) - Available modules and their APIs | ||
| - EOAKeyValidator - EOA owner validation | ||
| - WebAuthnValidator - Passkey/WebAuthn support | ||
| - SessionKeyValidator - Session key management with usage limits | ||
| - GuardianExecutor - Guardian-based account recovery | ||
| - [Registry](./registry.md) - ERC-7484 module registry integration | ||
| - [Calldata Format](./calldata-format.md) - ERC-7579 execution calldata encoding | ||
| - [Signature Formats](./signature-formats.md) - Signature encoding for each validator |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| # Architecture | ||
|
|
||
|  | ||
|
|
||
| The factory and all modules are behind `TransparentUpgradeableProxy`s. | ||
|
|
||
| Factory deploys `BeaconProxy`s that point to the `UpgradeableBeacon` which in turn has the account implementation address. | ||
|
|
||
| The following is a sequence diagram for the general SSO user flow: | ||
|
|
||
|  |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| # General ERC-7579 Calldata Format | ||
|
|
||
| There are 2 possible scenarios for a call from `EntryPoint` to the smart account: | ||
|
|
||
| 1. Call to a **core account method**, e.g. `installModule` | ||
| 2. Call to an **external address** | ||
|
|
||
| Calls to **core account methods** are trivially formatted: method selector + ABI encoded parameters. | ||
|
|
||
| Calls to **external addresses** are made via `execute(ModeCode code, bytes calldata data)` method. `code` is a `bytes32` value created according to the [standard](https://eips.ethereum.org/EIPS/eip-7579#execution-behavior): | ||
|
|
||
| - `callType` (1 byte): `0x00` for a single `call`, `0x01` for a batch `call`, `0xfe` for `staticcall` and `0xff` for `delegatecall` | ||
| - `execType` (1 byte): `0x00` for executions that revert on failure, `0x01` for executions that do not revert on failure but emit `event TryExecuteUnsuccessful(uint256 batchExecutionindex, bytes returnData)` on error | ||
| - unused (4 bytes): this range is reserved for future standardization | ||
| - `modeSelector` (4 bytes): an additional mode selector that can be used to create further execution modes, **currently unused** | ||
| - `modePayload` (22 bytes): additional data to be passed, **currently unused** | ||
|
|
||
| Depending on the value of the `callType`, data is one of the following: | ||
|
|
||
| - if `callType == CALLTYPE_SINGLE`, `data` is `abi.encodePacked(target, value, callData)` | ||
| - if `callType == CALLTYPE_DELEGATECALL`, `data` is `abi.encodePacked(target, callData)` | ||
| - if `callType == CALLTYPE_BATCH`, `data` is `abi.encode(executions)` where `executions` is an array `Execution[]` and | ||
|
|
||
| ```solidity | ||
| struct Execution { | ||
| address target; | ||
| uint256 value; | ||
| bytes callData; | ||
| } | ||
| ``` | ||
|
|
||
| ## Example | ||
|
|
||
| An external call to the contract `Storage` method `setValue(uint256 value)` with parameter 42 would have calldata as follows: | ||
|
|
||
| ```solidity | ||
| abi.encodeCall(IERC7579Account.execute, ( | ||
| bytes32(0), // callType: single, execType: default | ||
| abi.encodePacked( | ||
| storageAddress, // target | ||
| 0, // value | ||
| abi.encodeCall(Storage.setValue, (42)) // callData | ||
| ) | ||
| ) | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| # Deploying | ||
|
|
||
| ## Using forge | ||
|
|
||
| To deploy the contracts, use the `Deploy.s.sol` script. | ||
|
|
||
| To deploy the factory and 4 modules (`EOAKeyValidator`, `SessionKeyValidator`, `WebAuthnValidator` and `GuardianExecutor`): | ||
|
|
||
| ```bash | ||
| forge script script/Deploy.s.sol \ | ||
| --rpc-url $RPC_URL \ | ||
| --private-key $DEPLOYER \ | ||
| --broadcast | ||
| ``` | ||
|
|
||
| This should be used for if a new clean setup is desired (e.g. on a new network). | ||
| This will not deploy any accounts yet. | ||
|
|
||
| --- | ||
|
|
||
| To deploy an account from an existing factory with preinstalled modules: | ||
|
|
||
| ```bash | ||
| forge script script/Deploy.s.sol --sig 'deployAccount(address,address[])' $FACTORY_ADDRESS $MODULES_ADDRESSES \ | ||
| --rpc-url $RPC_URL \ | ||
| --private-key $DEPLOYER \ | ||
| --broadcast | ||
| ``` | ||
|
|
||
| This should be used to deploy an account when a factory is already deployed on the network, and/or a custom set of preinstalled modules is desired. | ||
|
|
||
| --- | ||
|
|
||
| To deploy everything at once (factory, all 4 default modules, and an account with all 4 default modules installed): | ||
|
|
||
| ```bash | ||
| forge script script/Deploy.s.sol --sig 'deployAll()' \ | ||
| --rpc-url $RPC_URL \ | ||
| --private-key $DEPLOYER \ | ||
| --broadcast | ||
| ``` | ||
|
|
||
| This should primarily be used during testing. | ||
|
|
||
| --- | ||
|
|
||
| In each case, admin of the factory and all modules will be the deployer. | ||
| For the account, the deployer's key will be registered as an EOA owner in the `EOAKeyValidator`. | ||
|
|
||
| Address of the new account can be found in the emitted event `AccountCreated(address indexed newAccount, address indexed deployer)`. | ||
|
|
||
| ## Manually | ||
|
|
||
| To deploy an account from an existing factory, call `deployAccount(bytes32 salt, bytes calldata initData)` on the factory. `initData` must be encoded in the following format: | ||
|
|
||
| ```solidity | ||
| address[] memory modules = ... // modules to be installed on the new account | ||
| bytes[] memory data = ... // initialization data for each module (empty if not needed) | ||
| initData = abi.encodeCall(IMSA.initializeAccount, (modules, data)) | ||
| ``` | ||
|
|
||
| Modules installed this way have to be of single type and must not repeat in the array. | ||
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,235 @@ | ||
| # Modules | ||
|
|
||
| Currently, only validator, fallback and executor module types are supported (as defined in the standard). | ||
|
|
||
| To install, uninstall or unlink a module, call the corresponding core functions on the account contract: | ||
|
|
||
| - `installModule(uint256 typeId, address module, bytes calldata initData)` | ||
| - `uninstallModule(uint256 typeId, address module, bytes calldata deinitData)` | ||
| - `unlinkModule(uint256 typeId, address module, bytes calldata deinitData)` | ||
|
|
||
| `typeId`, according to the [standard](https://eips.ethereum.org/EIPS/eip-7579): | ||
| - 1 for validator | ||
| - 2 for executor | ||
| - 3 for fallback | ||
|
|
||
| The account will call module's `onInstall(bytes)` hook upon installation and `onUninstall(bytes)` hook upon uninstall and unlink. The format for the supplied data is different for each module and is described below. | ||
|
|
||
| Unlinking is the same as uninstalling, but does not fail if `onUninstall` call to the module fails. Instead, error is emitted as `ModuleUnlinked(uint256 indexed typeId, address indexed module, bytes errorMsg)` event. | ||
|
|
||
| A single contract can house multiple module types, each type is installed separately. | ||
|
|
||
| ## `EOAKeyValidator` | ||
|
|
||
| Stores EOA addresses as account owners. Each address has full "admin" privileges to the account, as long as this validator is installed. | ||
|
|
||
| - `onInstall` data format: ABI-encoded array of addresses - initial owners | ||
| - `onUninstall` data format: ABI-encoded array of addresses - owners to remove | ||
|
|
||
| Other methods: | ||
| - `addOwner(address owner)` - adds an EOA owner, emits `OwnerAdded(address indexed account, address indexed owner)` | ||
| - `removeOwner(address owner)` - removes existing EOA owner, emits `OwnerRemoved(address indexed account, address indexed owner)` | ||
| - `isOwnerOf(address account, address owner) returns (bool)` - whether or not an address is an owner of the account | ||
|
|
||
| ## `WebAuthnValidator` | ||
|
|
||
| Stores WebAuthn passkeys per origin domain for each account. Each passkey has full "admin" privileges to the account, as long as this validator is installed. | ||
|
|
||
| - `onInstall` data format: ABI-encoded `(bytes credentialId, bytes32[2] publicKey, string domain)` - initial passkey, or empty | ||
| - `onUninstall` data format: ABI-encoded array of `(string domain, bytes credentialId)` - passkeys to remove | ||
|
|
||
| Other methods: | ||
| - `addValidationKey(bytes credentialId, bytes32[2] newKey, string domain)` - adds new passkey | ||
| - `removeValidationKey(bytes credentialId, string domain)` - removes existing passkey | ||
| - `getAccountKey(string domain, bytes credentialId, address account) returns (bytes32[2])` - account's public key on the domain with given credential ID | ||
| - `getAccountList(string domain, bytes credentialId) returns (address[])` - list of accounts on the domain with given credential ID (normally length of 1) | ||
|
|
||
| ## `SessionKeyValidator` | ||
|
|
||
| Grants a 3rd party limited access to the account with configured permissions. | ||
|
|
||
| A session is defined by the following structure: | ||
|
|
||
| ```solidity | ||
| struct SessionSpec { | ||
| address signer; | ||
| uint48 expiresAt; | ||
| UsageLimit feeLimit; | ||
| CallSpec[] callPolicies; | ||
| TransferSpec[] transferPolicies; | ||
| } | ||
| ``` | ||
|
|
||
| - `signer` - Address corresponding to an EOA private key that will be used to sign session transactions. **Signers are required to be globally unique.** | ||
| - `expiresAt` - Timestamp after which the session no longer can be used. **Session expiration is required to be no earlier than 60 seconds after session creation.** | ||
| - `feeLimit` - a `UsageLimit` (explained below) structure that limits how much fees this session can spend. **Required to not be `Unlimited`.** | ||
| - `callPolicies` - a `CallSpec` (explained below) array that defines what kinds of calls are permitted in the session. **The array has to have unique (`target`, `selector`) pairs.** | ||
| - `transferPolicies` - a `TransferSpec` (explained below) array that defines what kinds of transfers (calls with no calldata) are permitted in the session. **The array has to have unique targets.** | ||
|
|
||
| ### Usage Limits | ||
|
|
||
| All usage limits are defined by the following structure: | ||
|
|
||
| ```solidity | ||
| struct UsageLimit { | ||
| LimitType limitType; // can be Unlimited (0), Lifetime (1) or Allowance (2) | ||
| uint256 limit; // ignored if limitType == Unlimited | ||
| uint48 period; // ignored if limitType != Allowance | ||
| } | ||
| ``` | ||
|
|
||
| - `limitType` defines what kind of limit (if any) this is. | ||
| - `Unlimited` does not define any limits. | ||
| - `Lifetime` defines a cumulative lifetime limit: sum of all uses of the value in the current session has to not surpass `limit`. | ||
| - `Allowance` defines a periodically refreshing limit: sum of all uses of the value during the current `period` has to not surpass `limit`. | ||
| - `limit` - the actual number to limit by. | ||
| - `period` - length of the period in seconds. | ||
|
|
||
| ### Transfer Policies | ||
|
|
||
| Transfer policies are defined by the following structure: | ||
|
|
||
| ```solidity | ||
| struct TransferSpec { | ||
| address target; | ||
| uint256 maxValuePerUse; | ||
| UsageLimit valueLimit; | ||
| } | ||
| ``` | ||
|
|
||
| - `target` - address to which transfer is being made. | ||
| - `maxValuePerUse` - maximum value that is possible to send in one transfer. | ||
| - `valueLimit` - cumulative transfer value limit. | ||
|
|
||
| ### Call Policies | ||
|
|
||
| Call policies are defined by the following structure: | ||
|
|
||
| ```solidity | ||
| struct CallSpec { | ||
| address target; | ||
| bytes4 selector; | ||
| uint256 maxValuePerUse; | ||
| UsageLimit valueLimit; | ||
| Constraint[] constraints; | ||
| } | ||
| ``` | ||
|
|
||
| - `target` - address to which call is being made. | ||
| - `selector` - selector of the method being called. | ||
| - `maxValuePerUse` - maximum value that is possible to send in one call. | ||
| - `valueLimit` - cumulative call value limit. | ||
| - `constraints` - array of `Constraint` (explained below) structures that define constraints on method arguments. | ||
|
|
||
| ### Call Constraints | ||
|
|
||
| Call constraints are defined by the following structures: | ||
|
|
||
| ```solidity | ||
| struct Constraint { | ||
| Condition condition; | ||
| uint64 index; | ||
| bytes32 refValue; | ||
| UsageLimit limit; | ||
| } | ||
|
|
||
|
|
||
| enum Condition { | ||
| Unconstrained, | ||
| Equal, | ||
| Greater, | ||
| Less, | ||
| GreaterOrEqual, | ||
| LessOrEqual, | ||
| NotEqual | ||
| } | ||
| ``` | ||
|
|
||
| - `index` - index of the argument in the called method, starting with 0, assuming all arguments are 32-byte words after ABI-encoding. | ||
| - E.g., specifying `index: X` will constrain calldata bytes `4+32*X:4+32*(X+1)` | ||
| - `limit` - usage limit for the argument interpreted as `uint256`. | ||
| - `condition` - how the argument is required to relate to `refValue`: see `enum Condition` above. | ||
| - `refValue` - reference value for the condition; ignored if condition is `Unconstrained`. | ||
|
|
||
| ### `SessionKeyValidator` Methods | ||
|
|
||
| - `onInstall` data format: empty | ||
| - `onUninstall` data format: ABI-encoded array of session hashes to revoke | ||
|
|
||
| Other methods: | ||
| - `createSession(SessionSpec spec, bytes proof)` - create a new session; requires `proof` - a signature of the hash `keccak256(abi.encode(sessionHash, accountAddress))` signed by session `signer` | ||
| - `revokeKey(bytes32 sessionHash)` - closes an active session by the provided hash | ||
| - `revokeKeys(bytes32[] sessionHashes)` - same as `revokeKey` but closes multiple sessions at once | ||
| - `sessionStatus(address account, bytes32 sessionHash) returns (SessionStatus)` - returns `NotInitialized` (0), `Active` (1) or `Closed` (2); note: expired sessions are still considered active if not revoked explicitly | ||
| - `sessionState(address account, SessionSpec spec) returns (SessionState)` - returns the session status and the state of all cumulative limits used in the session as a following structure: | ||
|
|
||
| ```solidity | ||
| // Info about remaining session limits and its status | ||
| struct SessionState { | ||
| Status status; | ||
| uint256 feesRemaining; | ||
| LimitState[] transferValue; | ||
| LimitState[] callValue; | ||
| LimitState[] callParams; | ||
| } | ||
|
|
||
| struct LimitState { | ||
| uint256 remaining; // this might also be limited by a constraint or `maxValuePerUse`, which is not reflected here | ||
| address target; | ||
| bytes4 selector; // ignored for transfer value | ||
| uint256 index; // ignored for transfer and call value | ||
| } | ||
| ``` | ||
|
|
||
| Note: `sessionHash` is what is stored on-chain, and is defined by `keccak256(abi.encode(sessionSpec))`. | ||
|
|
||
| ## `GuardianExecutor` | ||
|
|
||
| Stores addresses trusted by the account to perform an EOA or WebAuthn key recovery. Either `EOAKeyValidator` or `WebAuthnValidator` must be installed. | ||
|
|
||
| The flow is the following: | ||
|
|
||
| ```mermaid | ||
| sequenceDiagram | ||
| actor Guardian | ||
| participant GuardiansExecutor | ||
| participant SmartAccount as SmartAccount (ERC-7579) | ||
| participant WebauthnValidator | ||
| actor User | ||
|
|
||
| User->>SmartAccount: proposeGuardian(guardian) | ||
| SmartAccount-->>WebauthnValidator: validate | ||
| WebauthnValidator-->>SmartAccount: ok | ||
| SmartAccount->>GuardiansExecutor: proposeGuardian(guardian) | ||
| Guardian->>GuardiansExecutor: acceptGuardian(account) | ||
|
|
||
| Note over User: Lose passkey | ||
|
|
||
| Guardian->>GuardiansExecutor: initializeRecovery(account, new passkey) | ||
| Note over Guardian: Wait 24 hours | ||
| Guardian->>GuardiansExecutor: finalizeRecovery(account, new passkey) | ||
| GuardiansExecutor->>SmartAccount: executeFromExecutor("add new passkey") | ||
| SmartAccount->>WebauthnValidator: addValidationKey(new passkey) | ||
| ``` | ||
|
|
||
| Important notes: | ||
| - Account owner has to first propose to another address to be its guardian | ||
| - After the guardian address accepts, it can initiate a recovery | ||
| - Recovery can be either for an EOA key or a Webauthn passkey, given that a corresponding validator is installed on the account | ||
| - Any guardian can initiate a recovery alone. Guardians can themselves be multisig accounts if that is desired | ||
| - A user can discard an initiated recovery in case one of the guardians is malicious | ||
| - Recovery can be finalized not earlier than 24 hours and not later than 72 hours after initiating it | ||
| - Only one recovery can be ongoing at a time | ||
|
|
||
| ### `GuardianExecutor` Methods | ||
|
|
||
| - `onInstall` data format: empty | ||
| - `onUninstall` data format: empty | ||
|
|
||
| Other methods: | ||
| - `proposeGuardian(address newGuardian)` - propose an address to be a guardian | ||
| - `acceptGuardian(address accountToGuard)` - an address that was proposed to can accept its role as a guardian | ||
| - `initializeRecovery(address accountToRecover, RecoveryType recoveryType, bytes data)` - initialize recovery of an EOA key (`recoveryType` 1) or passkey (`recoveryType` 2) of an account; `data` is ABI-encoded arguments to `EOAKeyValidator.addOwner` or `WebAuthnValidator.addValidationKey` | ||
| - `finalizeRecovery(address account, bytes data)` - finalize an ongoing recovery; the same data has to be passed in as was passed during initializing | ||
| - `discardRecovery()` - discard an ongoing recovery | ||
| - `guardianStatusFor(address account, address guardian) returns (bool isPresent, bool isActive)` - whether a given address was proposed to (is present) and has accepted (is active) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.