Skip to content
marsian83Public

About

Release signing for contracts and EIP-7702 delegates. Before you sign, your wallet checks the publisher's ENSv2 name and whether the release was withdrawn.

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

87 Commits

Folders and files

Repository files navigation

The word codemark in large letters over a dusk landscape, with a fox running past

Codemark

Know who published a contract before you sign it.

CI status MIT license CLI version on npm MCP server version on npm Snap version on npm
Built on ENSv2 on Sepolia Indexed on the Arkiv Rabdi testnet No custom Solidity Needs Bun 1.4.2 or newer Written in TypeScript

Try it  ·  Demo  ·  Docs  ·  How it works  ·  Live on Sepolia  ·  Security model

Codemark checks that the contract or account upgrade you are about to approve was released by the team you think, under their ENS name, and that the team still stands behind it.

Verified: released by pwnmeswap.eth, with all five steps of the trust path passing Don't sign: pwnmeswap.eth pulled this release, with the Release step in red and the team's reason

Paste a Sepolia contract address at codemark.hetairoi.xyz, or open the demo to see each result with a sample team. You don't need a wallet.

The problem

A wallet shows you 20 bytes and asks you to sign. In February 2025, Bybit's Safe signers approved a transaction that handed their wallet to an attacker's contract, because the web page that built the transaction had been swapped. Nothing on the signing screen could say "this is not code your team released".

An account upgrade (EIP-7702) is worse: one signature lets a contract control your whole account.

What you see

Every check ends in one of four verdicts: Verified, Released recently, Not verified or Don't sign, with the reason.

Below the verdict is the path it followed: publisher, release key, release, record, age. The step that failed is marked in red.

If a team's release key leaks, one transaction withdraws everything that key released, and every check after that shows it.

Four ways to use it

You are Use What you need
Anyone about to sign The lookup site at codemark.hetairoi.xyz A browser. No wallet, no sign-up
A MetaMask user The Codemark Snap, which checks every transaction before MetaMask asks you to confirm MetaMask Flask for now
Running an AI agent that spends The Codemark MCP server, which tells your agent to proceed, ask you or refuse before it signs Bun and an MCP-capable agent. It holds no keys
A team shipping contracts The CLI and the GitHub Action, which publish each release under your ENS name Sepolia ETH. init registers the name and sets up the team Safe for you

In your wallet. The Snap adds its result to the MetaMask confirmation. This is a real check of the demo router in MetaMask Flask.

Insights from Codemark in MetaMask Flask: released by pwnmeswap.eth, router 3.1.0. Publisher, release key, release, record and age all pass

For your agent. A real Claude Code session using the MCP server on Sepolia: proceed for a release from a team the user pinned, refuse for an account upgrade nobody released. No signer was attached, so nothing was signed.

A Claude Code session. The agent checks a router with Codemark, gets proceed and says it would sign. It then checks an EIP-7702 account upgrade, gets refuse and declines to sign

Get started

The full guide explains every command. The short version:

Check a contract

Open codemark.hetairoi.xyz/app, paste an address, and read the verdict. Pin the publishers you trust; only they can mark a contract as verified.

Check every transaction in MetaMask

Install MetaMask Flask in a separate browser profile, open the lookup site and select Install the Snap. It installs npm:@marsian/codemark-snap.

Protect an agent that spends

Nobody reads the signing screen when an agent signs, so a web page can hand it a lookalike router or an attacker's account upgrade. The MCP server checks the contracts behind each approval, swap and account upgrade first. Get a pin with bunx @marsian/codemark-mcp pin pwnmeswap.eth, then add the server to your agent:

{
  "mcpServers": {
    "codemark": {
      "command": "bunx",
      "args": ["@marsian/codemark-mcp"],
      "env": {
        "CODEMARK_PINS": "pwnmeswap.eth=0x8a8d97f3F4d1235c62aE61cDB3644f574162d204",
        "ARKIV_ACCESS_KEY": "your-key"
      }
    }
  }
}

Get a free key from the Arkiv hub. Without one, the server queries Arkiv's public endpoint, and once its query budget runs out, checks can time out and return refuse.

Then tell your agent to use the server, for example in its CLAUDE.md: "Before you sign anything, call Codemark's check_transaction or check_signature and do what its action says." In our three test runs, an agent that wasn't told to check never called the tool.

Publish your releases

You need Bun 1.4.2 or newer and three keys: a root key that owns the team Safe, a security key that can only withdraw things, and a CI key that can only add releases. Put them in .env in the folder you run the CLI from (CODEMARK_ROOT_KEY, CODEMARK_SEC_KEY, CODEMARK_CI_KEY), then:

bunx @marsian/codemark-cli init pwnmeswap --years 1                        # register pwnmeswap.eth, deploy the Safe and its registry
bunx @marsian/codemark-cli add-key pwnmeswap.eth ci-1 --ci 0x...           # let your CI address publish under ci-1
bunx @marsian/codemark-cli publish pwnmeswap.eth ci-1 --component router --version 3.1.0 --target 0x...   # release a deployed contract
bunx @marsian/codemark-cli verify 0x... --pin pwnmeswap.eth                # check it, the same way the site and the Snap do

There is nothing to install: bunx fetches the CLI when you run it.

If something goes wrong, pull withdraws one release and cut withdraws everything a key released, in one transaction each. The CLI reference lists every flag.

How it works

A publisher is an ENS name. Each signing key gets its own branch under that name, and each release is a name on that branch, recording the contract's address and code hash. A check walks that path on Sepolia, so the first step that fails is the one it shows you.

Summary diagram: a team's name tree with a CI key, a delegates key and a security key, next to the steps every check runs

If a key leaks, the team's security key removes its branch in one transaction, and every release under it stops verifying. Everything on chain is stock ENSv2; there is no custom Solidity.

Animation: ci-1 publishes and verifies, cutting ci-1 strikes out its releases and the wallet shows key revoked, then ci-2 is mounted and they verify again

How it works in detail covers the name tree, what each key can do and what a check proves.

Why ENSv2

  • Hierarchical registries. A key is a subtree, so one unregister ends every name below it. ENSv1 is flat: deleting ci-1.pwnmeswap.eth leaves router.ci-1.pwnmeswap.eth resolving.
  • EAC roles. Create-only CI key, remove-only security key, root-only delegates and reservations.
  • Expiry. A key's 90-day TTL bounds everything below it.
  • PermissionedResolver via VerifiableFactory. One resolver per release with root-only setters, which is what makes the "records are immutable to the CI key" proof possible.
  • Universal Resolver findResolver. An offset other than 0 means the name has no resolver of its own (unregistered, pulled, or under a cut key) and is rejected.
  • Token versions. Token ids change on every register and role change, so releases are keyed by labelhash and the current id is read from the registry.

Without ENSv2, a key couldn't be a subtree you revoke in one transaction, and teams would lose the name people already know them by.

Arkiv's role

Arkiv is an untrusted, searchable index. ENS can say whether a name is valid but cannot be searched by address. Arkiv adds discovery (which publisher claims an unknown address), withdrawal reasons (a pulled release otherwise looks never published), and the two proof block numbers per release. Rows are never trusted: each one is re-checked against ENS before it can change a verdict, and a missing or flooded index never turns anything green. Schemas, query examples and the trust model are in arkiv/README.md.

Two wallets wrote our entities on Rabdi: the CI key 0x47Fccf73d86473357c74c8ae393C7099Cdc06934 writes release rows, and the security key 0x703389487942788EC4AD8461A51e03a9F79F9186 writes incident rows.

This query lists who claims the demo router. See also: schema, friction, reach, why Arkiv and how we use it.

Live on Sepolia

pwnmeswap.eth is registered on the canonical ENSv2 Sepolia deployment, owned by a 1-of-1 Safe (0x5A0b…A9b0). Browse it on explorer.ens.domains.

The withdrawn release shows what a pull looks like: open 0x65aF…DAE5 in the demo and it says Don't sign, with the reason read from its Arkiv incident row. The router and portal, under the same key, still verify. No cut has run live, because it would withdraw those too; cuts are tested on an Anvil fork.

Every transaction behind the demo team
Step Name Transactions
Publisher registry, .eth commit and register pwnmeswap.eth 0x3a586172… 0xd866bc54… 0x30170433…
CI key (ROLE_REGISTRAR for the CI address) ci-1.pwnmeswap.eth 0x01e6b9b0… 0x1265ff6f…
Root-only delegates key delegates.pwnmeswap.eth 0xc6d15084… 0x67a669c3…
Router 3.1.0, published by CI router-3-1-0.ci-1.pwnmeswap.eth 0xe1b956a1… 0x2ae77cae…
Portal 2.0.0 (proxy, implementation pinned) portal-2-0-0.ci-1.pwnmeswap.eth 0x3ca5b578… 0x1e67f392…
EIP-7702 delegate 1.0.0, published by root delegate-1-0-0.delegates.pwnmeswap.eth 0x5213f6c6… 0x911e9e58…
Withdrawn demo release, published then pulled by root withdrawn-1-0-0.ci-1.pwnmeswap.eth 0xb1febb21… 0x5f43c019…

The router, portal and withdrawn release are indexed on Arkiv's Rabdi testnet; the delegate has none, because delegates are only trusted when pinned. Check any of the three targets with bunx @marsian/codemark-cli verify <address> --pin pwnmeswap.eth, in the demo, or on the lookup page after you pin pwnmeswap.eth.

Read the records yourself, without any Codemark code

Ask the canonical Universal Resolver through plain viem. Save this as a .ts file in packages/lib/shared, where viem is installed, and run it with bun. The address sits under the Sepolia coin type (ENSIP-11), and the codemark text record holds the code hash:

import { createPublicClient, http, toCoinType } from "viem";
import { sepolia } from "viem/chains";

const client = createPublicClient({ chain: sepolia, transport: http("https://sepolia.gateway.tenderly.co") });
const name = "router-3-1-0.ci-1.pwnmeswap.eth";
await client.getEnsAddress({ name, coinType: toCoinType(sepolia.id) }); // 0x3bfa4769fb09eefc5a80d6e87c3b9c650f7ae48e
await client.getEnsText({ name, key: "codemark" }); // v1;k=contract;n=router;c=11155111;h=0xe7f9…71de;v=3.1.0

Plain resolution cannot tell you whether the records could have been rewritten, whether the key is still trusted, or how old the release is. The checker adds those.

Limits

  • It tells you who published something, not whether the code is safe.
  • Both sides must adopt it: teams have to publish, and wallets have to run the check.
  • The Snap runs only in MetaMask Flask for now, and it has no Arkiv access key. When Arkiv's public endpoint is rate limiting, a release shows a warning instead of Verified. It never shows a false pass.
  • Mainnet is out of scope: Sepolia runs a different ENSv2 commit.
  • The MCP server protects an honest agent that was handed a bad address. A hijacked agent can skip the check, and the check doesn't cover amounts or recipients.

The security model covers each kind of attacker and every known limit.

More

How AI was used

Claude Code wrote most of the code, tests and docs across packages/ under our direction, including the WebGL landing scene. We set the problem, the security model and its limits, the product's wording and the visual direction, and reviewed the results. We also used Claude to generate and critique project ideas and to check our assumptions against the live Sepolia deployment. AGENTS.md and .claude/skills show the instructions it worked under.

License

MIT. .claude/skills/unslop is by Damir Sokolovsky under CC BY 4.0, as credited in that file.

About

Release signing for contracts and EIP-7702 delegates. Before you sign, your wallet checks the publisher's ENSv2 name and whether the release was withdrawn.

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages