EVM × Sui Walrus GitHub
Showcase 03 · implemented

An on-chain IPNS replacement, ENS-gated and kept alive by anyone.

A compact Solidity registry maps an ENS name to a Walrus blob pointer (blobId + Sui object id + content-type shortcode). Updates are authorized by the ENS registry's owner or an ENSIP-1 operator. A small TypeScript keeper extends the underlying Sui Blob object's end_epoch on a cron so the bytes survive indefinitely — and anyone can run a keeper for any name they're willing to pay WAL for.

Contract: WalrusResolver.sol Keeper: 03-walrus-resolver/keeper/ Stack: Solidity · viem · @mysten/sui · @mysten/walrus
01 · the pain

IPNS is the loudest broken primitive in the IPFS stack.

IPNS broken
ipfs-pain.md · §6
"Pinata is now openly pitching its smart-contract-based IPCM (IPFS CID Mapping) as a replacement for IPNS — a remarkable position from inside the IPFS commercial ecosystem."
— Pinata blog, Jan 2025
02 · the shape

One eth_call, one aggregator GET, no DHT walk.

setWalrusBlob(node, blobId, suiObjectId, contentType) is gated on ensRegistry.owner(node) == msg.sender || isApprovedForAll(owner, msg.sender), so ENSIP-1 operator approvals work out of the box. Three storage slots per name, one SSTORE-set per update.

walrusBlob(node) is a single eth_call. The reader follows the returned blobId to <aggregator>/v1/blobs/<blobId> for the bytes. The suiObjectId in the record is what a keeper extends to keep the blob alive forever.

The leverage over IPFS is not the trust model — it's that the pointer resolves deterministically in < 200ms instead of a DHT random walk.

// contract surface
function setWalrusBlob(
  bytes32 node,
  bytes32 blobId,
  bytes32 suiObjectId,
  bytes8  contentType   // e.g. "app/json"
) external;

function walrusBlob(bytes32 node)
  external view returns (
    bytes32 blobId,
    bytes32 suiObjectId,
    bytes8  contentType
  );

event WalrusBlobChanged(
  bytes32 indexed node,
  bytes32 blobId,
  bytes32 suiObjectId,
  bytes8  contentType,
  uint64  at
);
EVM read
aggregator GET
verified bytes
03 · try it locally

Deploy, point a subdomain, run the keeper.

Use a dedicated subdomain. ENS names have exactly one resolver record. Pointing vitalik.eth's resolver at WalrusResolver would lose every existing address / text / contenthash record on that name. Dedicate a subdomain such as blog.vitalik.eth to Walrus content and leave the apex name on its existing PublicResolver.
01

Build + test the contracts

cd showcases/contracts
forge install --no-git foundry-rs/forge-std openzeppelin/openzeppelin-contracts
forge build
forge test --match-contract WalrusResolver -vv  # 8 tests, all pass
02

Deploy WalrusResolver against the ENS registry

# Sepolia / mainnet / any L2 with an ENS-shaped registry
forge create src/WalrusResolver.sol:WalrusResolver \
  --rpc-url $EVM_RPC_URL \
  --private-key $DEPLOYER_PRIVATE_KEY \
  --constructor-args $ENS_REGISTRY

# mainnet ENS registry: 0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e
03

Create a subdomain and point its resolver at WalrusResolver

Via the ENS Manager on a name you own: create blog.vitalik.eth as a subdomain, then set its resolver to your WalrusResolver deployment address.

Programmatic: call ENSRegistry.setSubnodeOwner followed by ENSRegistry.setResolver.

04

Upload content to Walrus + set the pointer

# upload via @mysten/walrus (backend mode) — you must own the resulting Blob object
# to use the keeper. The publisher route works but the publisher owns the Blob, not you.

# once you have (blobId, suiObjectId), call:
cast send $RESOLVER_ADDRESS "setWalrusBlob(bytes32,bytes32,bytes32,bytes8)" \
  $(cast namehash blog.vitalik.eth) \
  $BLOB_ID \
  $SUI_OBJECT_ID \
  0x746578742f6d6400 \   # bytes8 = ASCII "text/md" + 1 null pad (16 hex chars)
  --rpc-url $EVM_RPC_URL \
  --private-key $OWNER_PRIVATE_KEY
05

Run the keeper

cd showcases/03-walrus-resolver/keeper
pnpm install
pnpm keeper

The keeper, for every name in KEEPER_NAMES, reads the on-chain pointer, fetches the linked Sui Blob's end_epoch, and calls executeExtendBlobTransaction when the remaining window drops below EXTENSION_THRESHOLD. Per-name failures are caught locally so one bad name does not abort the cron tick.

EnvRequired?Purpose
EVM_RPC_URLyesJSON-RPC for the chain holding WalrusResolver.
EVM_CHAIN_IDdefault 1EVM chain id (1 = mainnet, 11155111 = Sepolia, etc.).
RESOLVER_ADDRESSyesDeployed WalrusResolver address.
SUI_RPC_URLyesSui fullnode (e.g. https://fullnode.testnet.sui.io:443).
WALRUS_NETWORKdefault testnettestnet or mainnet.
SUI_PRIVATE_KEYyesbech32 keypair holding WAL to fund extensions.
KEEPER_NAMESyesComma-separated ENS names to keep alive.
EXTENSION_THRESHOLDdefault 5Refresh when remaining epochs < this.
EXTEND_BY_EPOCHSdefault 50Epochs to add per extension.
04 · verify

What "good" looks like.

05 · what this is NOT

Limits to set expectations.

  • Not a full ENS IResolver implementation. Only walrusBlob is exposed — no addr, no text, no contenthash. Consumers should staticcall walrusBlob(bytes32) directly, not via ENSIP resolver-record interface ids.
  • Not a Walrus pricing oracle. The keeper pays WAL out of its own Sui-side wallet; the EVM contract is unaware of WAL costs.
  • Not a hash verifier. Trust in the aggregator's bytes is identical to trust in an IPFS gateway. The leverage over IPFS comes from the pointer being deterministic, not the bytes.
  • Not a global namespace. Ownership is delegated to whichever ENS-shaped registry you pass at deploy time.
06 · source files

Where to look in the repo.