Smart contracts that publish Seed Protocol data as EAS attestations made by the user's smart account:
SeedProtocolExtension: a thirdweb ManagedAccount extension (called via the account's Router).SeedProtocolExecutor: an ERC-7579 executor module, plusSeedExecutorRouterExtension, which lets thirdweb accounts use it.
See Security model for who may publish, and docs/security/extension-access-control-plan.md for the reasoning behind it.
- Node 24 (see
.nvmrc) - Bun, the version pinned in
package.json(packageManager)
Use bun install, not npm install. Bun applies patches/, which the build needs (see Build notes).
bun install
bun run build # compile contracts and generate TypeChain types
bun run test # all tests (Mocha + ethers, in-process network)
bun run typecheck # type-check the config, Ignition modules, scripts and tests
bun run test:gas # tests with gas usage statistics
bun run coverage # tests with coverage (HTML report in coverage/html)Run a single suite with npx hardhat test mocha test/SeedProtocolExecutor.test.ts.
CI (.github/workflows/test.yml) runs build, type-check and tests on pushes to main and on pull requests.
| Path | Contents |
|---|---|
contracts/ |
Production contracts, plus mocks/MockERC7579Account.sol |
interfaces/ |
EAS and Seed Protocol interfaces |
test/ |
Test suites (*.test.ts) |
test/fixtures/ |
Shared fixtures: EAS + schemas, the thirdweb ManagedAccount stack (EntryPoint, factory, session keys, UserOps), gas payloads |
ignition/modules/ |
Ignition modules: SeedProtocol (what gets deployed) and LocalStack (a stand-in OP Sepolia for rehearsals) |
ignition/parameters/ |
Per-network module parameters, plus SeedRollout.factory for the rollout tasks |
infra/eas-indexer/ |
The official EAS indexer, pinned, for the twin |
ignition/deployments/ |
Ignition's deployment records: the one place deployed addresses live |
scripts/ |
Rollout tasks and the rehearsal driver (see Deployment); shared code in scripts/lib/ |
schemas/base-schemas.json |
The EAS schemas the protocol itself relies on, with their UIDs (for the SDK too) |
docs/ |
Plans and design notes |
This project uses Hardhat 3. Two things in the setup aren't obvious:
- The thirdweb patch.
@thirdweb-dev/dynamic-contractsimportslib/sstore2/...as a bare path. Hardhat 3 only resolves that through aremappings.txtinside the package itself, sopatches/adds one. Without it the build fails withHHE902. npmFilesToBuild. Hardhat 3 only writes artifacts for project sources, sohardhat.config.tslists the npm contracts the tests deploy by name: EAS, SchemaRegistry and the thirdweb account stack.
Compiler settings are solc 0.8.27, EVM target paris, optimizer 200 runs. These are the same as the earlier Hardhat 2 build, which produced identical bytecode (metadata hash aside).
Network credentials are read with Hardhat's configVariable(), from the environment or a local .env file:
| Variable | Used for |
|---|---|
OPTIMISM_SEPOLIA_RPC_URL, DEV_KEY |
--network optimism_sepolia |
OPTIMISM_SEPOLIA_RPC_URL |
rehearse:op-sepolia and twin:up (the forking node) |
ETHERSCAN_API_KEY |
ignition deploy --verify, hardhat verify |
They're only resolved when a task needs them; building and testing need none.
The Seed extensions run inside the user's thirdweb ManagedAccount: account fallback → factory Router → delegatecall into the extension. The Router does no authorization, so the extensions check callers themselves.
Who may call multiPublish (SeedAccountAuth):
- an account admin, calling the account directly;
- the account's EntryPoint, i.e. a UserOp signed by an admin;
- the account itself (
address(this)), viaexecute/executeBatch. This is how session keys publish.
Anyone else gets Unauthorized(caller).
Other rules:
- EAS is pinned. Each extension takes the EAS address in its constructor. There's no
setEas; changing EAS means deploying a new extension and callingreplaceExtensionon the factory. - Everything published is revocable.
multiPublishmakes every seed, version and property attestation revocable, whatever the request says, so the owner can always revoke what a delegate published. Seed and property schemas must be registered as revocable. - Cross-references name their target by position. In
propertiesToUpdate,publishIndexis the target request's index in the batch;localIdis carried for the client and never read. The contract reverts if the index is out of range, points at an earlier (already attested) request, or the target doesn't hold exactly one entry for the property. A client must compute indexes from the exact array it sends. - Delegates can't revoke. No path a session key can reach revokes attestations. Owners revoke directly with
execute(EAS, 0, revoke(...)). - The executor path (
SeedProtocolExecutorviaSeedExecutorRouterExtension) trusts one executor and one EAS, both fixed when the factory admin registers the extension. Account admins only opt in (installSeedExecutor) or out. The executor can onlyattest/multiAtteston that EAS, with value passed through exactly. - Factory changes need
EXTENSION_ROLEon the ManagedAccountFactory (replaceExtension,addExtension,removeExtension).
thirdweb's session keys let a third party publish as the account until the owner revokes them.
-
Grant. The account admin signs a
SignerPermissionRequestfor the delegate with a time window, a native-token limit andapprovedTargets:[account]to publish withmultiPublish;[executor]only, to publish through the executor (afterinstallSeedExecutor).
Never include EAS or
address(0): either lets the delegate call EAS directly, which bypasses forced revocability. -
Publish. The delegate signs a UserOp calling
execute(account, 0, multiPublish(...))(orexecute(executor, …)on the executor path). Calling the account directly from the session key, without a UserOp, is rejected. -
Revoke. The admin signs a new request with no targets, or lets the window expire. The EntryPoint then rejects the delegate's UserOps (
AA24).
test/ManagedAccountHarness.test.ts and test/SeedProtocolExtension.access.test.ts pin this down against the real account stack.
The plan, inputs and progress for the OP Sepolia rollout are in docs/deploy-plan.md. Contracts are deployed with Hardhat Ignition through CreateX, using a salt only the Seed deployer can use, so addresses are deterministic and can't be squatted.
bun run rehearse:localStarts a throwaway hardhat node, deploys LocalStack (EAS, EntryPoint, a ManagedAccountFactory with the old Seed registration, one account), then runs every rollout command below against it and publishes through the result.
bun run rehearse:op-sepolia -- --account <test account> --admin <its admin>The same against a node forking OP Sepolia, using the real factory from ignition/parameters/optimism_sepolia.json and impersonating its EXTENSION_ROLE holder. --account/--admin are optional; without them it only publishes from a fresh account it creates on the real factory.
bun run twin:upThis is a local stand-in for production, for developing the SDK and apps end to end (docs/local-twin-plan.md). It starts:
- a
hardhat nodeforking OP Sepolia at a pinned block, under chain id 31337, so nothing signed for it is valid on the real chain; - the rollout, applied with the real commands;
- funded test accounts;
- a local ERC-4337 bundler (alto) on
:4337.
It also starts the official EAS indexer (GraphQL on :4000, in Docker), seeded with OP Sepolia's data up to the fork. It then smoke-tests the publish paths and writes .twin/twin.json, with every endpoint, address, schema UID and test key the SDK and apps need. Ctrl-C stops it all.
-
Needs:
OPTIMISM_SEPOLIA_RPC_URL, and Docker for the indexer. -
Options:
--light-indexseeds schemas and names only;--no-indexerskips the indexer;--fork-block <n|latest>moves the fork.
-
bun run twin:fund <address…> [--eth <n>]gives ETH to accounts on the running twin. There's no paymaster. -
bun run twin:e2e [--sdk <path>]runs seed-protocol-sdk's own publish code (from../seed-protocol-sdkby default) against the running twin. It covers:- interactive and automation (executor) publishes, both with cross-references;
- the SDK's pre-send check, on a valid batch and on one the contract rejects;
- the EAS check, the executor install and the readiness check;
- session-key limits;
- owner revocation;
- the indexer.
Only thirdweb's hosted login is replaced, by a fresh local key. Run it before a rollout and after SDK changes.
-
For the SDK and apps: see docs/twin-thirdweb.md for connecting thirdweb wallets.
All take --network. The rollout tasks read ignition/parameters/<network>.json by default.
| Task | What it does | Writes? |
|---|---|---|
seed:ensure-schemas |
Registers the protocol's base EAS schemas (schemas/base-schemas.json) where missing. --check-only |
yes |
seed:predict-addresses |
Where the create2 deploy will put each contract, and whether CreateX would succeed there | no |
seed:extension-payload |
Prints the Router Extension structs for a deployment |
no |
seed:replace-extension |
Snapshots the factory's routing to routing-before.json, then replaces the Seed extension and adds the executor router in one multicall, and reads it all back. If the signer lacks EXTENSION_ROLE, prints the call to submit by hand. --dry-run, --check-only |
yes |
seed:verify-live --account <a> |
Static access-control and routing checks against a live account | no |
seed:publish-smoke |
Admin and session-key publishes through the live routing (simulated networks only) | yes |
seed:debug-publish --account <a> --payload <file> |
Simulates multiPublish and decodes the revert |
no |
seed:explain-userop --tx <hash> |
Explains a failed UserOp in a handleOps transaction: decodes its revert, or shows it ran out of gas and how much it needed |
no |
npx hardhat seed:predict-addresses --network optimism_sepolia
npx hardhat ignition deploy ignition/modules/SeedProtocol.ts --network optimism_sepolia --strategy create2 --parameters ignition/parameters/optimism_sepolia.json --verify
npx hardhat seed:replace-extension --network optimism_sepolia --dry-run
npx hardhat seed:replace-extension --network optimism_sepolia
npx hardhat seed:verify-live --network optimism_sepolia --account <test account>Rollback is replaceExtension back to the implementation in routing-before.json (and removeExtension("SeedExecutorRouterExtension")). The old implementations are vulnerable, so roll back only for a breaking failure.
scripts/get_4_byte_selectors.ts doesn't depend on Hardhat and runs with bun. scripts/decode_attestation_data.ts is an unported Hardhat 2 scratch script, kept pending review and left out of the type-check.