Repository navigation
Conversation
…metadata ERC721SeaDrop plus: - registerAgents: the owner registers an ERC-8004 agent for each unminted token through an ERC-8217 adapter, skipping tokens already registered. agentOf returns the agent id. - ERC8048OnchainMetadata (src/lib): ERC-8048 records with the Metadata Authority extension. Owner and allowed writers write while a token has no authority; the holder can set an authority to take over; transfers and burns clear it; the owner can lock a key for good. Every write emits MetadataSet and ERC-4906 MetadataUpdate. Tests use a mock adapter with the real adapter's registration rule, plus a fork test against the Base adapter that runs when AGENT_ADAPTER_FORK_URL is set. docs/AgentNFTs.md covers deployment, adapter addresses and how OpenSea shows agent NFTs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟢 Approval recommended
No active review issues block approval of the agent NFT extension.
No open issues 🎉
What changed in this PR
Adds an agent NFT extension to SeaDrop, combining pre-mint agent registration with reusable ERC-8048 onchain metadata while retaining existing minting and token URI behavior.
Changes:
- Adds range-based agent registration through an ERC-8217 adapter, with tracking to skip previously registered tokens.
- Introduces metadata writers, holder-controlled authorities, permanent key locks, and authority clearing on transfers and burns.
- Adds tests, an optional Base fork integration test, and deployment and usage documentation.
| File | Description |
|---|---|
test/foundry/ERC721SeaDropAgents.t.sol |
Tests metadata permissions, authority lifecycle, registration, and interface support. |
src/test/MockAgentAdapter.sol |
Provides an adapter mock enforcing registration permissions. |
src/lib/ERC8048OnchainMetadata.sol |
Implements reusable metadata storage, permissions, locks, and update events. |
src/interfaces/IERC8048MetadataAuthority.sol |
Defines the metadata authority getter and event. |
src/interfaces/IERC8048Metadata.sol |
Defines the metadata getter and write event. |
src/interfaces/IAgentAdapter.sol |
Defines the adapter registration interface. |
src/extensions/ERC721SeaDropAgents.sol |
Integrates agent registration and metadata into SeaDrop tokens. |
README.md |
Links to the agent NFT guide. |
docs/BringYourOwnTokenContract.md |
Introduces the Agents extension and links its guide. |
docs/AgentNFTs.md |
Documents deployment, registration, metadata rules, and OpenSea behavior. |
Review effort: Balanced
…fer behavior Writes before mint still emit MetadataSet but no longer emit MetadataUpdate for a token that doesn't exist, so pre-mint setup of a large collection doesn't send a refresh per record. A new _metadataTokenExists hook decides. Notes that a batch reverts if any token in it has an authority, and that records stay with a token when a transfer clears its authority. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🔵 Needs a closer look
Network-specific ERC-8217 adapter compatibility and the narrow EIP-170 bytecode margin require independent runtime validation, while dependency-based and fork tests could not be run in this review environment.
No open issues 🎉
Review effort: Balanced
Picks the adapter by chain id, so the test covers Ethereum, Base, Robinhood Chain and Sepolia. All four pass against public RPCs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Leave ERC-4906's MetadataUpdate to the token so the ERC-8048 library can sit next to OpenZeppelin's IERC4906, clear the authority of every token a transfer moves, and document the gas cap, what survives a sale, writer persistence, key locks and the adapter's upgradeability. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🔵 Needs a closer look
Irreversible registrations through external upgradeable adapters and narrow EIP-170 headroom require deployed-adapter compatibility and compiled-bytecode size validation unavailable in this source-only review.
No open issues 🎉
Review effort: Balanced
|
On the two points Copilot's latest review couldn't check from source alone (head 6f2cb27):
Nothing to change. |
… commit (#220) ## Motivation - On 2026-11-09 a GitHub releases outage returned 504 for Foundry's download, and every job on #219 failed at "Install Foundry" before building anything. Each job downloads Foundry again on every run. - Nothing in CI checks contract sizes. Several contracts are within a few kilobytes of EIP-170, and the agent extension in #212 is about 1 KB under it. - `cancel-in-progress: true` also applies to pushes on `main`, so when two PRs merge close together, the first merge commit's run is cancelled and never gets a result. ## Solution - `.github/actions/setup-foundry` restores `~/.foundry/bin` and `~/.foundry/versions` (foundryup keeps the binaries in `versions/` and links them from `bin/`) from the Actions cache, keyed on the runner OS and `FOUNDRY_VERSION`. It installs with `foundry-toolchain` only on a cache miss. All three jobs use it. - `scripts/check_sizes.sh` builds `src/` (skipping tests and scripts) with `forge build --sizes --json`. It fails if a contract it compiles is over 24,576 runtime bytes or 49,152 init code bytes. It prints the ten tightest margins, and writes them to the job summary, before failing. The test job runs it before the tests. - A new push to a pull request still cancels its older run. Every other run gets its own concurrency group (`github.run_id`), so no run on `main` or a tag is cancelled or replaced while it waits. - Jobs get timeouts: 15 minutes for lint and 20 for tests and coverage. - The `GOERLI_RPC_URL` secret is gone from the jobs, since Goerli is shut down and nothing reads it. - Third-party actions (`foundry-rs/foundry-toolchain`, `coverallsapp/github-action`) are pinned to commit SHAs. GitHub's own actions stay on major tags. ## Testing - `./scripts/check_sizes.sh` passes locally on `main` with Foundry 1.9.0. Its tightest margin is `ERC721SeaDropCloneable`, with 3,522 runtime bytes left. - `biome ci .` passes. - This PR's first run fills the Foundry cache, and a re-run should restore it. I'll confirm that on this PR. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
## Motivation The README's diagram was a PNG of ASCII art. It showed `mintAllowList` with a Merkle proof as the second mint path, and it left out `mintSigned`, the path OpenSea uses for allow lists. The README also said ERC-20 payments were a future idea, though v2 has them. ## Solution - The README's diagram is now Mermaid, so GitHub renders it and it can be edited as text. It shows the creator configuring the token, the token forwarding settings to `SeaDrop`, a minter paying `SeaDrop`, and `SeaDrop` paying out and calling `mintSeaDrop()`. A short paragraph under it names the functions. `img/seadrop-diagram.png` is deleted, and nothing else links to it, opensea-docs included. - Background now points to v2 for ERC-1155, ERC-20 payments and multiple stages, in place of the old "future" sentence. - `docs/SeaDropTokenDeployment.md`: - The `forge create` example gets `--broadcast`, which Foundry 1.x needs to send the transaction. - The section heading level is fixed. - The drop URI sentence uses "onchain" and has no dashes. - The README is shorter, 158 lines down to 94. It drops the table of contents and tightens Background and Deployments. Install, usage, the commands and the pull request checklist move to a new `docs/Development.md`, and the README keeps a three-command quick start that links it. The docs list is unchanged, so this doesn't conflict with #212's new line. ## Testing - I checked the rendered README on this branch on GitHub. The diagram renders and its labels don't overlap. - `biome ci .` passes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One collection authority (the owner when blank) writes any token, and two settings in the same slot control the rest: holderCanWrite lets a token's current holder write it, and authorityLocked stops the owner from changing the settings while the authority can still hand itself on. Drops setMetadataAuthority, metadata writers, the transfer hook and ERC-8048 Metadata Authority interface support. Adds MetadataWriterPolicy, a reference policy that allows several writers and lets a holder delegate their token's writes to one address until the token moves. Based on Prem's proposal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🔵 Needs a closer look
Irreversible metadata locks and authority policies cross an upgradeable-adapter trust boundary, warranting human security review, with deployed-adapter integration and the narrow EIP-170 code-size margin not independently validated.
No open issues 🎉
1 resolved since last review
Review effort: Balanced
A key lock also froze the key for tokens not minted yet, so a token minted after the lock could never get a value. Drops lockMetadataKey from the module and leaves freezing to a controller set as the collection authority. MetadataWriterPolicy gets setWriteOnceKey: a write to a write-once key reverts once that token has a non-empty value, through setMetadata and setMetadataBatch alike. A collection that wants every record frozen locks the authority to 0x...dEaD. From Prem's follow-up review. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rename collectionAuthority() to tokenMetadataAuthority() and emit TokenMetadataAuthoritySet, answer 0x04f90b41 in supportsInterface, make a set authority the only writer, add ownerCannotWrite, and allow locking a blank authority so the settings hold for good. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>


Adds
ERC721SeaDropAgents, an extension for collections whose tokens are AI agents, modeled on what MUSEGOD's Muses contract added toERC721SeaDropon Robinhood Chain. The ERC-8048 code is a separate module insrc/lib, so other token contracts can reuse it.Agent registration
registerAgents(firstTokenId, lastTokenId, prefix, suffix)registers an ERC-8004 agent for each token through an ERC-8217 adapter, with agent URIprefix+ id +suffix. Whoever holds the token controls the agent. The adapter is a constructor argument, since it is only deployed on some networks.agentOf(tokenId)returns it.ERC-8048 onchain metadata
ERC8048OnchainMetadatastores string keys with bytes values per token and implements ERC-8048's optional Metadata Authority extension in its collection-wide form (ethereum/ERCs#2174). The authority and three settings pack into one storage slot.tokenMetadataAuthority()returns the one address that can write every token, including before mint, or zero when none is set. While it is set it is the only writer, as the ERC requires: the owner and holders write only through it.supportsInterface(0x04f90b41)returns true, and every change of the address emitsTokenMetadataAuthoritySet(address indexed authority).owner(), so it follows ownership) writes any token unlessownerCannotWriteis on.holderCanWritelets a token's current holder (ownerOfat write time) write that token. Transfers need no claim: the buyer writes right away and the seller can't. ERC-721 operators can't write for the holder.authorityLockedstops the owner from callingsetCollectionAuthority, including while the owner writes as the blank authority. A set authority can still call it, so a locked policy can hand itself on. A blank authority can be locked, which fixes the settings for good and blocks adding a policy later.setCollectionAuthority(authority, holderCanWrite, ownerCannotWrite, authorityLocked)sets everything and emitsCollectionAuthoritySettingsUpdated(holderCanWrite, ownerCannotWrite, authorityLocked), so indexers see flag changes too.collectionAuthoritySettings()returns the flags. They default to false, so a new collection starts owner-only.artist.signatureleaves every later token unsigned. Freezing is left to a policy set as the authority, which can freeze per token. A collection that wants every record frozen locks a blank authority withownerCannotWriteon andholderCanWriteoff (testFreezeEverything).MetadataSet. A write to a minted token also emits ERC-4906MetadataUpdate, since OpenSea refreshes on ERC-4906 events. The library calls a_emitMetadataUpdatehook and the token declares the event, so the library also works next to OpenZeppelin'sIERC4906. Writes before mint skip it. A batch emits oneMetadataUpdateper run of entries for the same token, and reverts if the caller can't write any entry.The settings cover these states, each with a test:
testOwnerOnlyByDefaultholderCanWritetestHoldersWriteholderCanWrite,authorityLockedtestLockedBlankOwnerAndHoldersholderCanWrite,ownerCannotWrite,authorityLockedtestLockedBlankHoldersOnlyownerCannotWrite,authorityLockedtestFreezeEverythingtestPolicyAuthority,testAuthorityIsOnlyWriterauthorityLockedtestLockedPolicyThis replaces the earlier per-token Metadata Authority design.
lockMetadataKey,metadataKeyLocked,MetadataKeyLocked,MetadataKeyIsLocked,setMetadataAuthority, the per-token authority mapping, metadata writers (setMetadataWriter), the clear-on-transfer hook, and ERC-165 support for the per-token Metadata Authority draft (0xf9cb127d) are gone. The token reports ERC-8048 (0xdf670be1), its collection-wide Metadata Authority extension (0x04f90b41), and ERC-4906 (0x49064906). Transfers no longer touch metadata storage.Reference policy
src/lib/MetadataWriterPolicy.solis a small example to set as the collection authority, so nothing the per-token design offered is lost. The token doesn't depend on it.TwoStepOwnableowner) allows several writer addresses withsetWriter, replacingsetMetadataWriter.setDelegate, such as their agent's wallet. The delegation records the holder who set it and counts only while that address is stillownerOf, so it lapses on transfer (and counts again if the token returns to that holder).setWriteOnceKey(key, bool), and a write to that key reverts withWriteOnceValueSet(tokenId, key)when the token already has a non-empty value, read through the token'smetadata(tokenId, key).setMetadataBatchapplies the same rule and also counts earlier entries in the batch, so a batch can't set a value and overwrite it. Tokens minted later can still be written once (testPolicyWriteOnceKey,testPolicyWriteOnceKeyBatch). The admin can unmark a key, so a lasting freeze locks the authority to the policy and renounces the policy. While the policy is the authority, holders can't write past it whateverholderCanWritesays.setMetadataandsetMetadataBatchcheck the caller and forward to the token. The batch checks array lengths first (BatchLengthMismatch), since it now reads each entry.setCollectionAuthorityis an admin passthrough, so a policy can hand the authority on.renounceOwnershipalso cancels a pending ownership transfer, as the token does, so a renounced policy stays renounced.The module reserves no keys. ERC-8048 is a draft and its agent profile (ERC-721T) may still change, so the keys are only listed in the doc. Token id 0 is not special.
Docs
docs/AgentNFTs.md covers deployment, the adapter addresses on Ethereum, Base, Robinhood Chain and Sepolia (each confirmed to have code), registration, the metadata authority and its common setups, the reference policy, and how OpenSea shows agent NFTs. The README and the bring-your-own-token doc link to it.
Size and gas
ERC721SeaDropAgentsruntime code is 23,472 bytes, 1,104 bytes under EIP-170.Tests
test/foundry/ERC721SeaDropAgents.t.sol: 32 tests (31 run, 1 fork test skipped in CI) covering the states above, settings permissions, whenTokenMetadataAuthoritySetfires, a set authority as the only writer,ownerCannotWritewhile unlocked, a locked authority clearing itself to a locked blank state, writes before mint, holder writes across transfer, safe transfer and burn, batches, the reference policy's writers, holder delegation and write-once keys (single and batch) through the token, registration ranges and skips (including a collection whose ids start past 1), interface ids, and the library next to OpenZeppelin'sERC721URIStorage.forge coverage --ir-minimumreports 100% line and branch coverage ofERC8048OnchainMetadataandERC721SeaDropAgents, and 100% branch coverage ofMetadataWriterPolicy(its two-linerenounceOwnershipshows as unhit lines under--ir-minimum, thoughtestPolicyRenounceCancelsPendingOwnercalls it).src/test/MockAgentAdapter.solapplies the real adapter's registration rule.testRegisterAgentsWithDeployedAdapterforks the chainAGENT_ADAPTER_FORK_URLnames and registers through that chain's deployed adapter. It is skipped in CI. It passes locally on Ethereum, Base, Robinhood Chain and Sepolia against public RPCs.Audit
An independent review of the earlier version found no critical, high or medium issues. Its changes that still apply: the library doesn't declare
MetadataUpdate(so it compiles next to OpenZeppelin'sIERC4906), and the docs cover the 16,777,216-gas transaction cap, that nobody can change an agent URI before mint, that agent URIs are public once registered, that the agent's URI and wallet carry over a sale, and that the adapters are upgradeable proxies a 3-of-4 Safe controls on Ethereum and Base. Its findings about clearing per-token authorities on transfer, batches failing on tokens with an authority, and writers surviving an ownership transfer no longer apply: there is no per-token authority, the collection authority can write every batch entry, key locks are gone, and writers now live in a policy contract with its own admin.Not in this PR
🤖 Generated with Claude Code