Account
The Porto Account is a keychain that holds user funds, enforces permissions via Keys, manages nonces to prevent replay attacks, and enables secure executions from the account.
Concepts
Keys
A key is a fundamental signing unit. An account can authorize
multiple keys with different limits and permissions.
/// @dev A key that can be used to authorize call.
struct Key {
/// @dev Unix timestamp at which the key expires (0 = never).
uint40 expiry;
/// @dev Type of key. See the {KeyType} enum.
KeyType keyType;
/// @dev Whether the key is a super admin key.
/// Super admin keys are allowed to call into super admin functions such as
/// `authorize` and `revoke` via `execute`.
bool isSuperAdmin;
/// @dev Public key in encoded form.
bytes publicKey;
}
Key Types
/// @dev The type of key.
enum KeyType {
P256,
WebAuthnP256,
Secp256k1,
External
}
The account supports 4 key types natively -
- P256: Standard ECDSA key on the
secp256r1
curve. Mainly used for browser session keys. - WebAuthnP256: Enables passkey support, using the webauthn standard.
- Secp256k1: Standard ECDSA key on the
secp256k1
curve. Can be used directly with Ethereum EOA private keys. - External: Allows devs to extend the verification capabilities of the account, by calling an external
Signer
contract for signature verification.
Key Hashes
Each key in the account is uniquely identified by its keyHash.
The keyHash is calculated as -
bytes32 keyHash = keccak256(abi.encode(key.keyType, keccak256(key.publicKey)))
Public key encoding
The encoding of a key pair's public key depends on the key type:
Key Type | Encoding Format | Description |
---|---|---|
secp256r1 (P256) | abi.encode(x, y) | Stores both x and y coordinates for the secp256r1 curve. |
webAuthn | abi.encode(x, y) | Stores both x and y coordinates of the public key on the elliptic curve. |
secp256k1 | abi.encode(address) | Stores only the Ethereum address derived from the public key (truncated hash). |
external | abi.encode(address(signer), bytes12(salt)) | Stores the address of the external signer, and a bytes12 salt value |
Signature encoding
The signature is encoded as follows: abi.encodePacked(bytes(innerSignature), bytes32(keyHash), bool(prehash))
, where the key hash is keccak(bytes32(keyType, publicKey))
.
The inner signature depends on the key type:
Key Type | Signature |
---|---|
secp256r1 (p256) | (r, s) |
webauthn | (r, s) |
secp256k1 | (r, s) or (r, vs) |
Super Admin Keys
- Highest permission tier in the account. Can
authorize
andrevoke
any other keys. - Only super admin keys are allowed to sign 1271
isValidSignature
data. - The EOA private key is automatically considered a super admin key.
External Key Type
Nonce Management
The account supports 4337-style 2D nonce sequences.
A nonce is a uint256
value, where the first 192 bits are the sequence key
and the remaining 64 bits are treated as sequential incrementing nonces.
Example:
- If
nonce = 1
:
sequence key = 0
incrementing value = 1
- Next valid nonce for this sequence key:
2
- If
nonce = (1 << 64) + 1
(i.e., 264 + 1):
sequence key = 1
incrementing value = 1
- Next valid nonce for this sequence key:
(1 << 64) + 2
It is recommended to use separate sequence keys for different backend services, to allow parallel transactions to go through.
MultiChain Prefix
When a nonce's sequence key begins with the prefix 0xc1d0
(a mnemonic for "chainID zero"), the Porto Account recognizes this as a multichain execution. Consequently, the chainId
is omitted from the EIP-712 domain separator when constructing the digest for signature verification.
This allows the same signature to be valid across multiple chains.
Execution
The Porto Account uses the ERC 7821 Executor interface.
Executions are accepted in the form of Calls
/// @dev Call struct for the `execute` function.
struct Call {
address to; // Replaced as `address(this)` if `address(0)`.
uint256 value; // Amount of native currency (i.e. Ether) to send.
bytes data; // Calldata to send with the call.
}
The execution interface is
function execute(bytes32 mode, bytes calldata executionData) public payable;
Modes
The Porto Account supports the following execution modes.
0x01000000000000000000...
: Single batch. Does not support optionalopData
.0x01000000000078210001...
: Single batch. Supports optionalopData
.0x01000000000078210002...
: Batch of batches.
Delegate calls are not supported.
Execution senders and opData
The exact Op data depends on who is calling the execute
function.
Self Call & EOA
No op data is needed, if this is a self call. This can happen in 2 cases -
- The account performs recursive calls to
execute
. Administrative functions such asauthorize
andrevoke
utilize this self-call pattern and require careful handling. - The sender is the 7702 authority
Orchestrator Intents
The orchestrator is given some special privileges in the account. These are discussed in the Orchestrator Integration section.
One of these privileges is the ability to verify signature & increment nonces before calling execute
on the account.
Therefore, the opData
if the orchestrator is the sender is structured as
bytes opData = abi.encode(bytes32 keyHash)
This execution type is exclusively used by the intent flow.
Others
Any other external caller, has to provide a nonce and a signature for any execution they want to do on the account.
Therefore, the opData
is structured as
bytes opData = abi.encodePacked(uint256 nonce, bytes signature)
Example
The execution data for a batch of calls being sent by an arbitrary sender would look like this
Call memory call = Call({
to: <address>,
value: 0,
data: <swap tokens>
});
uint256 nonce = account.getNonce(0); // 0 is the uint192 sequence key
bytes memory signature = _sign(computeDigest(calls, nonce));
bytes memory opData = abi.encodePacked(nonce, signature);
bytes memory executionData = abi.encode(calls, opData);
account.execute(_ERC7821_BATCH_EXECUTION_MODE, executionData);
Orchestrator Integration
At the time of deployment, an orchestrator address can be set in a porto account. The orchestrator is an immutable privileged entity, that facilitates trustless interactions between the relayer and the account.
To do this, it is given 3 special access points into the account. More details about the whole intent flow can be found in the Orchestrator documentation.
1. Pay
/// @dev Pays `paymentAmount` of `paymentToken` to the `paymentRecipient`.
function pay(
uint256 paymentAmount,
bytes32 keyHash,
bytes32 intentDigest,
bytes calldata encodedIntent
) public;
Allows the orchestrator to transfer the paymentAmount
specified in the intent signed by the user, pre and post execution.
2. Check and Increment Nonce
/// @dev Checks current nonce and increments the sequence for the `seqKey`.
function checkAndIncrementNonce(uint256 nonce) public payable;
Checks if the nonce
specified in the intent is valid, and increments the sequence if it is.
3. Execute
As discussed in the execution section above, the orchestrator verifies the intent signature and increments the nonce before calling execute
.
So for execute calls coming from the orchestrator, these checks are skipped in the account.
Endpoints
Admin
These functions are marked public virtual onlyThis
, meaning they can only be called by the account itself. To invoke them, a super admin key must submit a transaction to the execute
function, with the calls
parameter encoding a call to one of these admin functions.
setLabel
function setLabel(string calldata newLabel) public virtual onlyThis
- Access Control: The account itself (via
execute
from an authorized super admin key). - Description: Sets or updates the human-readable label for the account. Emits a
LabelSet
event. -
Usage:
- Include a call to this function in the
calls
array of anexecute
transaction. newLabel
: The new string label for the account.
- Include a call to this function in the
revoke
function revoke(bytes32 keyHash) public virtual onlyThis
- Access Control: The account itself (via
execute
from an authorized super admin key). - Description: Revokes an existing authorized key. Removes the key from storage and emits a
Revoked
event. -
Usage:
- Include a call to this function in the
calls
array of anexecute
transaction. keyHash
: The hash of the key to be revoked. The key must exist.
- Include a call to this function in the
authorize
function authorize(Key memory key) public virtual onlyThis returns (bytes32 keyHash)
- Access Control: The account itself (via
execute
from an authorized super admin key). - Description: Authorizes a new key or updates the expiry of an existing key. Emits an
Authorized
event. -
Usage:
- Include a call to this function in the
calls
array of anexecute
transaction. key
: AKey
struct containing:expiry
: Unix timestamp for key expiration (0 for never).keyType
: Type of key (P256
,WebAuthnP256
,Secp256k1
,External
).isSuperAdmin
: Boolean indicating if the key has super admin privileges. Note:P256
key type cannot be super admin.publicKey
: The public key bytes.
- Returns the
keyHash
of the authorized key.
- Include a call to this function in the
setSignatureCheckerApproval
function setSignatureCheckerApproval(bytes32 keyHash, address checker, bool isApproved) public virtual onlyThis
- Access Control: The account itself (via
execute
from an authorized super admin key). - Description: Approves or revokes an address (
checker
) to successfully validate signatures for a givenkeyHash
viaisValidSignature
. Emits aSignatureCheckerApprovalSet
event. -
Usage:
- Include a call to this function in the
calls
array of anexecute
transaction. keyHash
: The hash of the key for which the checker approval is being set. The key must exist.checker
: The address of the contract or EOA being approved/revoked.isApproved
:true
to approve,false
to revoke.
- Include a call to this function in the
invalidateNonce
function invalidateNonce(uint256 nonce) public virtual onlyThis
- Access Control: The account itself (via
execute
from an authorized super admin key). - Description: Invalidates all nonces for a given sequence key up to and including the provided
nonce
. The upper 192 bits ofnonce
act as the sequence key (seqKey
). Emits aNonceInvalidated
event. -
Usage:
- Include a call to this function in the
calls
array of anexecute
transaction. nonce
: The nonce to invalidate. The lower 64 bits are the sequential part, and the upper 192 bits are the sequence key.
- Include a call to this function in the
upgradeProxyAccount
function upgradeProxyAccount(address newImplementation) public virtual onlyThis
- Access Control: The account itself (via
execute
from an authorized super admin key). - Description: Upgrades the implementation of the proxy account if this account is used with an EIP-7702 proxy. It calls
LibEIP7702.upgradeProxyDelegation
and then callsthis.upgradeHook()
on the new implementation. -
Usage:
- Include a call to this function in the
calls
array of anexecute
transaction. newImplementation
: The address of the new account implementation contract. The new implementation should have anupgradeHook
function.
- Include a call to this function in the
upgradeHook
function upgradeHook(bytes32 previousVersion) external virtual onlyThis returns (bool)
- Access Control: The account itself, specifically called during the
upgradeProxyAccount
process by the old implementation on the new implementation's context. It includes a guard to ensure it's called correctly. - Description: A hook function called on the new implementation after an upgrade. It's intended for storage migrations or other setup tasks. The current version is a no-op but demonstrates the pattern.
-
Usage:
- This function is not called directly by users. It's part of the upgrade mechanism.
previousVersion
: The version string of the old implementation.
Execution
Discussed here
Signature Validation
unwrapAndValidateSignature
function unwrapAndValidateSignature(bytes32 digest, bytes calldata signature) public view virtual returns (bool isValid, bytes32 keyHash)
-
Description:
- Checks if the Orchestrator is paused.
- If the signature is 64 or 65 bytes, it's treated as a raw secp256k1 signature from
address(this)
. - Otherwise, it attempts to unwrap a packed signature:
abi.encodePacked(bytes(innerSignature), bytes32(keyHash), bool(prehash))
. - If
prehash
is true,digest
is re-hashed withsha256
. - Validates the
innerSignature
against thedigest
using the public key associated with the unwrappedkeyHash
and itskeyType
. SupportsP256
,WebAuthnP256
,Secp256k1
(delegated to an EOA), andExternal
(delegated to another contract implementingisValidSignatureWithKeyHash
). - Checks for key expiry.
-
Usage:
digest
: The digest that was signed.signature
: The signature data, potentially wrapped.- Returns
isValid
(boolean) and thekeyHash
used for validation.
isValidSignature
function isValidSignature(bytes32 digest, bytes calldata signature) public view virtual returns (bytes4)
- Description: Implements EIP-1271. Checks if a given signature is valid for the provided digest.
- It first unwraps and validates the signature using
unwrapAndValidateSignature
. - If valid, it further checks if the key used is a super admin key OR if
msg.sender
is an approved checker for that key hash. - This restriction (super admin or approved checker) is to prevent session keys from approving infinite allowances via Permit2 by default.
- It first unwraps and validates the signature using
-
Usage:
- Called by other contracts (e.g., Permit2, DEXes) to verify signatures on behalf of this account.
digest
: The hash of the message that was signed.signature
: The wrapped signature (abi.encodePacked(bytes(innerSignature), bytes32(keyHash), bool(prehash))
) or a raw secp256k1 signature.- Returns
0x1626ba7e
if valid,0xffffffff
if invalid.
View
Functions to read data from the account.
getNonce
function getNonce(uint192 seqKey) public view virtual returns (uint256)
- Description: Returns the current nonce for a given sequence key. The full nonce is
(uint256(seqKey) << 64) | sequential_nonce
. -
Usage:
seqKey
: The upper 192 bits of the nonce, identifying the nonce sequence.- Returns the full 256-bit nonce, where the lower 64 bits are the next sequential value to be used.
label
function label() public view virtual returns (string memory)
- Description: Returns the human-readable label of the account.
- Usage: Call to retrieve the account's label.
keyCount
function keyCount() public view virtual returns (uint256)
- Description: Returns the total number of authorized keys (including potentially expired ones before filtering in
getKeys
). - Usage: Call to get the count of all registered key hashes.
keyAt
function keyAt(uint256 i) public view virtual returns (Key memory)
- Description: Returns the
Key
struct at a specific indexi
from the enumerable set of key hashes. -
Usage:
i
: The index of the key to retrieve.- Useful for enumerating keys off-chain, but
getKeys()
is generally preferred for fetching all valid keys.
getKey
function getKey(bytes32 keyHash) public view virtual returns (Key memory key)
- Description: Returns the
Key
struct for a givenkeyHash
. Reverts if the key does not exist. -
Usage:
keyHash
: The hash of the key to retrieve.
getKeys
function getKeys() public view virtual returns (Key[] memory keys, bytes32[] memory keyHashes)
- Description: Returns two arrays: one with all non-expired
Key
structs and another with their correspondingkeyHashes
. - Usage: Call to get a list of all currently valid (non-expired) authorized keys.
getContextKeyHash
function getContextKeyHash() public view virtual returns (bytes32)
- Description: Returns the
keyHash
of the key that authorized the current execution context (i.e., the most recent key in the_KEYHASH_STACK_TRANSIENT_SLOT
). Returnsbytes32(0)
if the EOA key was used or if not in an execution context initiated by a key. - Usage: Can be called by modules or hooks executed via
execute
to determine which key authorized the call.
approvedSignatureCheckers
function approvedSignatureCheckers(bytes32 keyHash) public view virtual returns (address[] memory)
- Description: Returns an array of addresses that are approved to use
isValidSignature
for the givenkeyHash
. -
Usage:
keyHash
: The hash of the key.
rPREP
(View Function)
function rPREP() public view virtual returns (bytes32)
- Description: Returns the
r
value of the PREP (Pre-Executed Proxy) signature, if the account has been initialized as a PREP. Returns0
if not initialized. - Usage: Used in conjunction with EIP-7717 (PREP) for counterfactual proxy deployment and initialization.
isPREP
function isPREP() public view virtual returns (bool)
- Description: Returns
true
if the account has been correctly initialized as a PREP and therPREP
value is set and valid. - Usage: To check if the account is a valid PREP.
Helpers
These functions are helpers that can be called publicly.
hash
(Key Hashing)
function hash(Key memory key) public pure virtual returns (bytes32)
- Description: Computes the
keyHash
for a givenKey
struct. The hash iskeccak256(abi.encode(key.keyType, keccak256(key.publicKey)))
. Note thatexpiry
andisSuperAdmin
are not part of this hash. -
Usage:
key
: TheKey
struct to hash.- Useful for deriving a
keyHash
off-chain before authorization or for verification.
computeDigest
function computeDigest(Call[] calldata calls, uint256 nonce) public view virtual returns (bytes32 result)
- Description: Computes the EIP-712 typed data hash for an
Execute
operation.- If the
nonce
starts withMULTICHAIN_NONCE_PREFIX
(0xc1d0), the digest is computed without the chain ID (for multichain replay protection). - Otherwise, the standard EIP-712 digest including the chain ID is computed.
- If the
-
Usage:
calls
: Array ofCall
structs to be executed.nonce
: The nonce for this execution.- The returned digest should be signed by an authorized key to authorize the execution.
Initialization
initializePREP
function initializePREP(bytes calldata initData) public virtual returns (bool)
Can be called by anyone, but typically by the Orchestrator or a deployer as part of the PREP (EIP-7717) initialization flow.
- Description: Initializes the account as a Pre-Executed Proxy (PREP).
- If already initialized (i.e.,
rPREP
is non-zero), it returnstrue
. - Decodes
initData
(expected to be ERC7821-style batch execution:abi.encode(calls, abi.encodePacked(bytes32(saltAndAccount)))
). - Computes and stores the
rPREP
value. - Executes the
calls
decoded frominitData
internally.
- If already initialized (i.e.,
-
Usage:
initData
: Encoded data containing calls to be executed upon initialization and the salt/account info for PREP.- This function allows the account to be set up and its initial state configured atomically with its PREP validation.
- Reverts if
initData
is invalid or if the address is not a valid PREP address (resulting inr == 0
).