|
| 1 | +# Modules |
| 2 | + |
| 3 | +Currently, only validator, fallback and executor module types are supported (as defined in the standard). |
| 4 | + |
| 5 | +To install, uninstall or unlink a module, call the corresponding core functions on the account contract: |
| 6 | + |
| 7 | +- `installModule(uint256 typeId, address module, bytes calldata initData)` |
| 8 | +- `uninstallModule(uint256 typeId, address module, bytes calldata deinitData)` |
| 9 | +- `unlinkModule(uint256 typeId, address module, bytes calldata deinitData)` |
| 10 | + |
| 11 | +`typeId`, according to the [standard](https://eips.ethereum.org/EIPS/eip-7579): |
| 12 | +- 1 for validator |
| 13 | +- 2 for executor |
| 14 | +- 3 for fallback |
| 15 | + |
| 16 | +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. |
| 17 | + |
| 18 | +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. |
| 19 | + |
| 20 | +A single contract can house multiple module types, each type is installed separately. |
| 21 | + |
| 22 | +## `EOAKeyValidator` |
| 23 | + |
| 24 | +Stores EOA addresses as account owners. Each address has full "admin" privileges to the account, as long as this validator is installed. |
| 25 | + |
| 26 | +- `onInstall` data format: ABI-encoded array of addresses - initial owners |
| 27 | +- `onUninstall` data format: ABI-encoded array of addresses - owners to remove |
| 28 | + |
| 29 | +Other methods: |
| 30 | +- `addOwner(address owner)` - adds an EOA owner, emits `OwnerAdded(address indexed account, address indexed owner)` |
| 31 | +- `removeOwner(address owner)` - removes existing EOA owner, emits `OwnerRemoved(address indexed account, address indexed owner)` |
| 32 | +- `isOwnerOf(address account, address owner) returns (bool)` - whether or not an address is an owner of the account |
| 33 | + |
| 34 | +## `WebAuthnValidator` |
| 35 | + |
| 36 | +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. |
| 37 | + |
| 38 | +- `onInstall` data format: ABI-encoded `(bytes credentialId, bytes32[2] publicKey, string domain)` - initial passkey, or empty |
| 39 | +- `onUninstall` data format: ABI-encoded array of `(string domain, bytes credentialId)` - passkeys to remove |
| 40 | + |
| 41 | +Other methods: |
| 42 | +- `addValidationKey(bytes credentialId, bytes32[2] newKey, string domain)` - adds new passkey |
| 43 | +- `removeValidationKey(bytes credentialId, string domain)` - removes existing passkey |
| 44 | +- `getAccountKey(string domain, bytes credentialId, address account) returns (bytes32[2])` - account's public key on the domain with given credential ID |
| 45 | +- `getAccountList(string domain, bytes credentialId) returns (address[])` - list of accounts on the domain with given credential ID (normally length of 1) |
| 46 | + |
| 47 | +## `SessionKeyValidator` |
| 48 | + |
| 49 | +Grants a 3rd party limited access to the account with configured permissions. |
| 50 | + |
| 51 | +A session is defined by the following structure: |
| 52 | + |
| 53 | +```solidity |
| 54 | +struct SessionSpec { |
| 55 | + address signer; |
| 56 | + uint48 expiresAt; |
| 57 | + UsageLimit feeLimit; |
| 58 | + CallSpec[] callPolicies; |
| 59 | + TransferSpec[] transferPolicies; |
| 60 | +} |
| 61 | +``` |
| 62 | + |
| 63 | +- `signer` - Address corresponding to an EOA private key that will be used to sign session transactions. **Signers are required to be globally unique.** |
| 64 | +- `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.** |
| 65 | +- `feeLimit` - a `UsageLimit` (explained below) structure that limits how much fees this session can spend. **Required to not be `Unlimited`.** |
| 66 | +- `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.** |
| 67 | +- `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.** |
| 68 | + |
| 69 | +### Usage Limits |
| 70 | + |
| 71 | +All usage limits are defined by the following structure: |
| 72 | + |
| 73 | +```solidity |
| 74 | +struct UsageLimit { |
| 75 | + LimitType limitType; // can be Unlimited (0), Lifetime (1) or Allowance (2) |
| 76 | + uint256 limit; // ignored if limitType == Unlimited |
| 77 | + uint48 period; // ignored if limitType != Allowance |
| 78 | +} |
| 79 | +``` |
| 80 | + |
| 81 | +- `limitType` defines what kind of limit (if any) this is. |
| 82 | + - `Unlimited` does not define any limits. |
| 83 | + - `Lifetime` defines a cumulative lifetime limit: sum of all uses of the value in the current session has to not surpass `limit`. |
| 84 | + - `Allowance` defines a periodically refreshing limit: sum of all uses of the value during the current `period` has to not surpass `limit`. |
| 85 | +- `limit` - the actual number to limit by. |
| 86 | +- `period` - length of the period in seconds. |
| 87 | + |
| 88 | +### Transfer Policies |
| 89 | + |
| 90 | +Transfer policies are defined by the following structure: |
| 91 | + |
| 92 | +```solidity |
| 93 | +struct TransferSpec { |
| 94 | + address target; |
| 95 | + uint256 maxValuePerUse; |
| 96 | + UsageLimit valueLimit; |
| 97 | +} |
| 98 | +``` |
| 99 | + |
| 100 | +- `target` - address to which transfer is being made. |
| 101 | +- `maxValuePerUse` - maximum value that is possible to send in one transfer. |
| 102 | +- `valueLimit` - cumulative transfer value limit. |
| 103 | + |
| 104 | +### Call Policies |
| 105 | + |
| 106 | +Call policies are defined by the following structure: |
| 107 | + |
| 108 | +```solidity |
| 109 | +struct CallSpec { |
| 110 | + address target; |
| 111 | + bytes4 selector; |
| 112 | + uint256 maxValuePerUse; |
| 113 | + UsageLimit valueLimit; |
| 114 | + Constraint[] constraints; |
| 115 | +} |
| 116 | +``` |
| 117 | + |
| 118 | +- `target` - address to which call is being made. |
| 119 | +- `selector` - selector of the method being called. |
| 120 | +- `maxValuePerUse` - maximum value that is possible to send in one call. |
| 121 | +- `valueLimit` - cumulative call value limit. |
| 122 | +- `constraints` - array of `Constraint` (explained below) structures that define constraints on method arguments. |
| 123 | + |
| 124 | +### Call Constraints |
| 125 | + |
| 126 | +Call constraints are defined by the following structures: |
| 127 | + |
| 128 | +```solidity |
| 129 | +struct Constraint { |
| 130 | + Condition condition; |
| 131 | + uint64 index; |
| 132 | + bytes32 refValue; |
| 133 | + UsageLimit limit; |
| 134 | +} |
| 135 | +
|
| 136 | +
|
| 137 | +enum Condition { |
| 138 | + Unconstrained, |
| 139 | + Equal, |
| 140 | + Greater, |
| 141 | + Less, |
| 142 | + GreaterOrEqual, |
| 143 | + LessOrEqual, |
| 144 | + NotEqual |
| 145 | +} |
| 146 | +``` |
| 147 | + |
| 148 | +- `index` - index of the argument in the called method, starting with 0, assuming all arguments are 32-byte words after ABI-encoding. |
| 149 | + - E.g., specifying `index: X` will constrain calldata bytes `4+32*X:4+32*(X+1)` |
| 150 | +- `limit` - usage limit for the argument interpreted as `uint256`. |
| 151 | +- `condition` - how the argument is required to relate to `refValue`: see `enum Condition` above. |
| 152 | +- `refValue` - reference value for the condition; ignored if condition is `Unconstrained`. |
| 153 | + |
| 154 | +### `SessionKeyValidator` Methods |
| 155 | + |
| 156 | +- `onInstall` data format: empty |
| 157 | +- `onUninstall` data format: ABI-encoded array of session hashes to revoke |
| 158 | + |
| 159 | +Other methods: |
| 160 | +- `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` |
| 161 | +- `revokeKey(bytes32 sessionHash)` - closes an active session by the provided hash |
| 162 | +- `revokeKeys(bytes32[] sessionHashes)` - same as `revokeKey` but closes multiple sessions at once |
| 163 | +- `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 |
| 164 | +- `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: |
| 165 | + |
| 166 | +```solidity |
| 167 | +// Info about remaining session limits and its status |
| 168 | +struct SessionState { |
| 169 | + Status status; |
| 170 | + uint256 feesRemaining; |
| 171 | + LimitState[] transferValue; |
| 172 | + LimitState[] callValue; |
| 173 | + LimitState[] callParams; |
| 174 | +} |
| 175 | +
|
| 176 | +struct LimitState { |
| 177 | + uint256 remaining; // this might also be limited by a constraint or `maxValuePerUse`, which is not reflected here |
| 178 | + address target; |
| 179 | + bytes4 selector; // ignored for transfer value |
| 180 | + uint256 index; // ignored for transfer and call value |
| 181 | +} |
| 182 | +``` |
| 183 | + |
| 184 | +Note: `sessionHash` is what is stored on-chain, and is defined by `keccak256(abi.encode(sessionSpec))`. |
| 185 | + |
| 186 | +## `GuardianExecutor` |
| 187 | + |
| 188 | +Stores addresses trusted by the account to perform an EOA or WebAuthn key recovery. Either `EOAKeyValidator` or `WebAuthnValidator` must be installed. |
| 189 | + |
| 190 | +The flow is the following: |
| 191 | + |
| 192 | +```mermaid |
| 193 | +sequenceDiagram |
| 194 | + actor Guardian |
| 195 | + participant GuardiansExecutor |
| 196 | + participant SmartAccount as SmartAccount (ERC-7579) |
| 197 | + participant WebauthnValidator |
| 198 | + actor User |
| 199 | +
|
| 200 | + User->>SmartAccount: proposeGuardian(guardian) |
| 201 | + SmartAccount-->>WebauthnValidator: validate |
| 202 | + WebauthnValidator-->>SmartAccount: ok |
| 203 | + SmartAccount->>GuardiansExecutor: proposeGuardian(guardian) |
| 204 | + Guardian->>GuardiansExecutor: acceptGuardian(account) |
| 205 | +
|
| 206 | + Note over User: Lose passkey |
| 207 | +
|
| 208 | + Guardian->>GuardiansExecutor: initializeRecovery(account, new passkey) |
| 209 | + Note over Guardian: Wait 24 hours |
| 210 | + Guardian->>GuardiansExecutor: finalizeRecovery(account, new passkey) |
| 211 | + GuardiansExecutor->>SmartAccount: executeFromExecutor("add new passkey") |
| 212 | + SmartAccount->>WebauthnValidator: addValidationKey(new passkey) |
| 213 | +``` |
| 214 | + |
| 215 | +Important notes: |
| 216 | +- Account owner has to first propose to another address to be its guardian |
| 217 | +- After the guardian address accepts, it can initiate a recovery |
| 218 | +- Recovery can be either for an EOA key or a Webauthn passkey, given that a corresponding validator is installed on the account |
| 219 | +- Any guardian can initiate a recovery alone. Guardians can themselves be multisig accounts if that is desired |
| 220 | +- A user can discard an initiated recovery in case one of the guardians is malicious |
| 221 | +- Recovery can be finalized not earlier than 24 hours and not later than 72 hours after initiating it |
| 222 | +- Only one recovery can be ongoing at a time |
| 223 | + |
| 224 | +### `GuardianExecutor` Methods |
| 225 | + |
| 226 | +- `onInstall` data format: empty |
| 227 | +- `onUninstall` data format: empty |
| 228 | + |
| 229 | +Other methods: |
| 230 | +- `proposeGuardian(address newGuardian)` - propose an address to be a guardian |
| 231 | +- `acceptGuardian(address accountToGuard)` - an address that was proposed to can accept its role as a guardian |
| 232 | +- `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` |
| 233 | +- `finalizeRecovery(address account, bytes data)` - finalize an ongoing recovery; the same data has to be passed in as was passed during initializing |
| 234 | +- `discardRecovery()` - discard an ongoing recovery |
| 235 | +- `guardianStatusFor(address account, address guardian) returns (bool isPresent, bool isActive)` - whether a given address was proposed to (is present) and has accepted (is active) |
0 commit comments