Security and session keys
Who can publish and revoke through the Seed contracts, why everything is revocable, how session keys are scoped, what a publishAuthorization attestation records, and what a leaked session key can and can't do.
The Seed contracts run inside the user's thirdweb ManagedAccount, so their security model is about one question: who can make the account attest, and who can undo it.
Who's involved
- The account is the attester of everything, and so the author. See Identity and signing.
- Account admins control the account. With the default thirdweb setup, the admin is the user's in-app wallet.
- The EntryPoint executes ERC-4337 UserOps once the account has validated their signature.
- Session keys are signers the admin has granted limited permissions. thirdweb's account enforces those limits when it validates a UserOp.
- The factory admin decides which extensions every account created by the factory routes to. More on that below.
Who can call what
The extensions run by delegatecall from the account's Router fallback, and the Router itself authorizes nothing, so each function checks its caller (SeedAccountAuth). An unauthorized caller gets Unauthorized(caller).
| Function | Allowed callers |
|---|---|
multiPublish on the account (the extension) | an account admin calling the account directly; the EntryPoint (a UserOp an admin signed); the account calling itself through execute/executeBatch, which is how an admin's or a session key's UserOp reaches it |
installSeedExecutor, uninstallSeedExecutor | an account admin or the EntryPoint only. Not the account calling itself, so a session key can't reach them |
executeFromExecutor (the router extension) | only the executor the factory admin pinned, and only while the account has it installed |
multiPublish, publish, createSeed, createVersion on the executor | whoever can make the account call the executor: admins, and session keys whose approved targets include the executor. The executor acts for its caller, the account |
getEas, getSeedExecutor, isModuleInstalled | anyone (views) |
executeFromExecutor lets the executor make the account call only EAS's attest or multiAttest, never a revoke, and the executor's install hooks stop a session key from re-pointing it at another EAS; see executeFromExecutor and Lifecycle and views.
Everything published is revocable
Every Seed, Version and property attestation created through multiPublish, on either path, is made with revocable: true, whatever the request says. The request's seedIsRevocable and each entry's revocable field are ignored.
That's what guarantees the owner can always undo what a delegate published. It also means every schema the Seed contracts attest under must be registered as revocable: EAS rejects a revocable attestation on an irrevocable schema. The publish package registers every schema it creates as revocable; see Base schemas.
Two other properties hold for the same reason:
- EAS is pinned. The extension and the router extension fix the EAS address at deployment, so nothing an account or a delegate does can point them at another contract; see EAS is fixed at deployment.
- Property attestations point at their Version. A property entry with a zero
refUIDgets the new Version's UID. A non-zerorefUIDfrom the client is kept.
Revoking is owner-only
EAS only lets an attestation's attester revoke it, and the attester is the account. No path a session key can reach calls EAS's revoke or multiRevoke: the executor has no revoke function, and the router extension refuses every EAS selector except attest and multiAttest.
The owner revokes by having the account call EAS directly, execute(EAS, 0, multiRevoke(...)), in a UserOp the admin signs. That's what the SDK's item.unpublish() sends; it refuses up front if the connected wallet is an automation session key. See Revocation.
Why can't a delegate revoke only what it published? Because nothing on-chain records which signer made an attestation. EAS records the account, and once a UserOp is validated the account doesn't pass the signer on. Any revoke function a delegate could reach could revoke all of the account's attestations, the owner's included.
Session keys
A session key lets an app publish as the user's account while the user isn't there. It's a thirdweb signer permission on the ManagedAccount, set by the admin, with:
- approved targets: the contracts the key's UserOps may call through
execute/executeBatch; - a native-token limit per transaction;
- a time window.
What the publish package grants
enrollPublishAutomation from @seedprotocol/publish grants:
| Permission | Value |
|---|---|
| Approved targets | the Seed executor only (modularAccountModuleContract) |
| Native-token limit | 0 per transaction |
| Window | until expiresAt, which defaults to 90 days after enrollment. The attestation below gets the same value |
Before adding the key, it installs the executor on the account with installSeedExecutor, from the user's admin wallet, and checks that the executor can act for the account. The key then publishes with a UserOp calling execute(executor, 0, multiPublish(...)). Publish unattended with a session key walks through it.
After the factory moves to a new executor
A key's approved target is the executor's address, so a key approved for an old executor can't reach the new one, and through the old one it gets NotSeedExecutor. The account's admin installs the new executor and grants the key again; enrollPublishAutomation does both. permissionsHash covers the targets, so a new grant means a new publishAuthorization attestation.
Until the admin installs, a key approved for the new executor can't initialize it: onInstall reverts while the account doesn't report it installed. A key approved for the old executor can still call that executor's onUninstall on an account that has moved on, which clears only configuration nothing uses.
What the contracts allow
The contracts also accept a session key whose approved targets include the account itself: it then publishes with execute(account, 0, multiPublish(...)). That's broader than the executor-only grant, because such a key can call anything that trusts the account calling itself, which today is multiPublish and thirdweb's setContractURI (account metadata only). The publish package won't use such a key: an automation publish fails with MODULAR_SIGNER_ACTIVATION_FAILED unless the key is approved for the configured executor and nothing else.
Never include EAS or address(0) (thirdweb's wildcard) in a session key's approved targets. Either lets the key call EAS directly as the account: it could make irrevocable attestations, and it could revoke anything the account ever published.
Withdrawing a key
The admin withdraws a key by replacing its permissions with an empty target list (revokePublishAutomation does this), or by letting its window end. From then on the EntryPoint rejects the key's UserOps (AA24). Withdrawing a key stops future publishes; it doesn't undo past ones.
What a leaked session key can and can't do
With the executor-only grant the publish package makes, someone holding the key can, until it's withdrawn or expires:
- publish as the account through the executor: any schema, any data, any number of attestations. That includes edits to the user's items, and attestations under schemas other apps read as claims;
- spend gas on those UserOps, paid by the paymaster when gas is sponsored, or by the account when it isn't.
They can't:
- revoke anything, or make an irrevocable attestation;
- send ETH from the account (the limit is
0); - call EAS, the SchemaRegistry or any other contract directly, so they can't register schemas either;
- install, uninstall or re-point the executor;
- add signers, change their own permissions or touch the account's admins;
- change which code the account routes to.
The contracts have no per-account schema allowlist and no per-delegate permissions, so a key can publish under any schema and only the owner can revoke; Roadmap records the ideas for both.
To recover, withdraw the key and revoke what it published. Since EAS records only the account, use the key's active window and your app's own logs to find those attestations; the chain can't tell you which signer made each one.
publishAuthorization attestations
When enrollPublishAutomation grants a key, it also makes a seedprotocol.publishAuthorization attestation from the account, so the grant is public and discoverable on EAS. revokePublishAutomation removes the key and then revokes that attestation.
| Field | Type | Value |
|---|---|---|
identity | address | the ManagedAccount |
sessionKey | address | the session key (also the attestation's recipient) |
app | address | the appAddress you passed, or the zero address |
scopes | string | publish,revoke, a fixed string in this version |
grantedAt | uint64 | Unix seconds |
expiresAt | uint64 | the grant's expiresAt, or 0 |
permissionsHash | bytes32 | keccak-256 of the sorted approved targets, the native-token limit, expiresAt and the scopes |
The schema is address identity,address sessionKey,address app,string scopes,uint64 grantedAt,uint64 expiresAt,bytes32 permissionsHash, with no resolver, revocable. Its UID and registration are on Base schemas.
The attestation is a record, not a permission. The contracts never read it: what limits the key is the signer permission on the account. Readers can check a grant with assessPublishAuthorizationLive from @seedprotocol/publish, which reports a revoked grant as 'revoked' and one past its expiry as 'expired'.
scopes says publish,revoke, but a session key can't revoke. Treat the field as informational.
Trusting the factory admin
A ManagedAccount is "managed" because its code comes from the factory: the account's fallback asks the factory's Router which implementation handles each function. Whoever holds EXTENSION_ROLE on the ManagedAccountFactory can add, replace or remove extensions, and that changes the code running inside every account the factory created. The same role holder chooses which executor and which EAS the router extension trusts; account admins only opt in or out of the executor.
So the Seed contracts protect users from other users and from delegates, but users trust the factory admin. Deployments says who holds the factory's roles on OP Sepolia, and Roadmap the plan to move them to a multisig. The contracts the factory routes today replaced earlier implementations with access-control issues; see History.
Related
- Identity and signing: the account model behind all of this
- Publishing and revoking: enrolling and using a session key
- Revocation
- Seed Protocol extension and Executor and router extension
- Cross-references
- Deployments: addresses and who holds the factory roles
Cross-references (publishIndex)
How one multiPublish batch writes a Seed UID it creates into a property or list element of any request in the batch, the rules the contracts enforce, how the SDK builds a batch, and how direct EAS differs.
Base schemas
The EAS schemas Seed Protocol relies on beyond your own models: their definitions and UIDs, which ones must exist before anyone can publish, and which the SDK registers on demand.