Anchor contract
Scope
Fifty lines of Solidity that do two things: record that a publisher committed to the state of its log on a given day, and record that a subject withdrew one of its own claims. Nothing else belongs in it.
It is optional everywhere it appears. Every other document in this lab works with files and DNS alone, and an implementation that cannot reach a chain is a complete implementation.
What it does not hold: no claim, no licence reference, no name, no domain, no attestation, no personal data of any kind. Putting a credential onchain would make it permanent and public, which is the opposite of what a credential needs.
Interface
interface IRevowAnchor {
event Anchored(address indexed publisher, uint32 indexed day, bytes32 root, uint64 at);
event Revoked(address indexed subject, bytes32 indexed claimId, uint64 at);
function anchor(uint32 day, bytes32 root) external;
function revoke(bytes32 claimId) external;
function revokedAt(address subject, bytes32 claimId) external view returns (uint64);
error EmptyRoot();
error AlreadyRevoked();
}Full source: RevowAnchor.sol. ABI: RevowAnchor.abi.json, taken from the compiler output rather than written by hand. Both MIT.
Anchoring
A publisher builds a Merkle tree over the digests of every document it published that day, in sequence order, and calls anchor with the day number and the root. The day is days since the Unix epoch in UTC, which is a plain integer and needs no date library to compare.
Anchoring costs one event and writes no storage. That is deliberate. The event is the record, consumers index it, and no later call can overwrite or hide an earlier one.
Anchoring the same day twice is allowed. Both entries stay visible with their own timestamps, which is what an amendment should look like: a correction that admits there was something to correct, rather than a value quietly replaced.
A publisher that stops anchoring is a signal in itself, and a visible one. Silence in a log that used to be anchored daily is information a consumer can act on.
Withdrawal
A subject calls revoke with the identifier of its own claim. The sender is the subject, so no signature is carried and no one, including whoever deployed the contract, can withdraw on another subject behalf. The time is stored as well as emitted, because a consumer about to display a claim needs to answer the question in one cheap call rather than by replaying logs.
Withdrawal is one of the four decay signals in RVW-6. The other three need no chain at all.
Using it
import { createPublicClient, createWalletClient, http } from "viem";
import { mainnet } from "viem/chains";
const REVOW_ANCHOR = "0x6aA4bfb9f4E370FB9596C09828dEfC819468A792";
// Anchor yesterday log. day is days since the Unix epoch, UTC.
const day = Math.floor(Date.UTC(2026, 10, 8) / 86400000);
await wallet.writeContract({ address: REVOW_ANCHOR, abi, functionName: "anchor",
args: [day, merkleRoot] });
// Withdraw one of your own claims.
await wallet.writeContract({ address: REVOW_ANCHOR, abi, functionName: "revoke",
args: [claimId] });
// Before displaying a claim, in one call.
const at = await client.readContract({ address: REVOW_ANCHOR, abi,
functionName: "revokedAt", args: [subject, claimId] });
if (at !== 0n) return WITHDRAWN;Selectors and topics
anchor(uint32,bytes32) 0x58ca1623
revoke(bytes32) 0xb75c7dc6
revokedAt(address,bytes32) 0x02673e5c
Anchored(address,uint32,bytes32,uint64)
0x52ad64dc6832f9580b78f32e3455794bb1aea82c40db57407aab746b5bf986b8
Revoked(address,bytes32,uint64)
0x1ca9c43de7d16e40f0f81485e4c96447bf4737a4bd8fd05787b04880167ee79c
EmptyRoot() 0x53ce4ece
AlreadyRevoked() 0x905e7107Computed from the signatures, so they can be checked without trusting this page. The address to point an indexer at is in section 08.
How it was deployed
Reproduced here so that anyone can rebuild the bytecode and compare it against what the explorer holds. Verification fails if any of these differ.
| Setting | Value |
|---|---|
| Compiler | solc 0.8.24 |
| Optimizer | on, 200 runs |
| Pipeline | via IR |
| EVM version | paris |
| Constructor arguments | none |
| Libraries | none |
Deployment cost 156,191 gas at 0.2 gwei. The deploying key received no privilege of any kind and has nothing left to do.
Deployments
| Chain | Address | Deploy block | Source |
|---|---|---|---|
| Ethereum mainnet | 0x6aA4bfb9f4E370FB9596C09828dEfC819468A792 | 26,379,202 | Verified |
Deploy transaction 0xb9f5da96cb9998d531e5…. The source on the explorer is byte for byte the file published here as RevowAnchor.sol.
A later version of this contract is a new deployment under a new specification number, never an upgrade of this one. This address cannot change its own rules, has no owner and no administrator, and will behave in ten years exactly as it behaves today.
Limits
Anchoring daily costs gas every day, which is a running cost a publisher has to want. An anchor writes no storage and emits one log with two indexed topics. A withdrawal writes one fresh storage slot, which is twenty thousand gas on its own, plus a log. Call it roughly twenty four thousand gas to anchor and forty six thousand to withdraw, both including the twenty one thousand base cost of a transaction, and expect those figures to be replaced here by measured ones once there is something to measure.
Withdrawal is also public: the fact that a subject withdrew a claim, and when, is visible to everyone forever, and for a routine key rotation that is more disclosure than the event deserves. How to get immediate withdrawal without that exposure is open problem OP-7.