EVM × Sui Walrus GitHub
Showcase 04 · implemented

Snapshot's pattern, the proposer pays WAL, the contract holds 32 bytes.

A 100-line Solidity contract holds the on-chain skeleton of a vote — proposer, deadline, yes/no tallies — while the human-readable proposal body lives on Walrus. The proposer pays WAL once via the public publisher; the DAO contract never holds WAL. Anyone reads the proposal anonymously through any Walrus aggregator.

Contract: Governance.sol CLIs: 04-dao-proposals/ Stack: Solidity · viem · Walrus publisher API
01 · the pain

Snapshot ships every Aave / Uniswap proposal through hosted IPFS.

hosted IPFS
ipfs-pain.md · §9
"4Everland is used by Snapshot governance for Aave and Uniswap DAO votes" — bundled into the same hosted-IPFS tier that absorbs the operational pain because public gateways can't be trusted.
— 2026 builder-sentiment survey
02 · the shape

The DAO never touches WAL. Voters never touch IPFS.

Showcase-only voting surface. Governance.vote() reads voteToken.balanceOf(msg.sender) at the instant of the call. Against any ERC-20 with a flash-mint or flash-borrow integration, this is exploitable: an attacker can borrow tokens, vote, and repay in a single transaction. For production, pair with an OpenZeppelin ERC20Votes token and switch vote() to IVotes(voteToken).getPastVotes(msg.sender, proposalStartBlock).

propose(blobId, deadline) takes a 32-byte Walrus blob id and a unix-seconds deadline. The contract reverts on past deadlines and zero blob ids, then stores the Proposal(proposer, blobId, deadline, yes, no) struct and emits Proposed.

vote(id, support) reads the caller's voteToken balance, gates on !hasVoted && block.timestamp < deadline, and accumulates the weight on yes or no.

tally(id) is the canonical read for (yes, no, passed, closed). The proposal body is fetched from <aggregator>/v1/blobs/<blobIdBase64Url> — no Snapshot adapter, no pinning vendor.

// Governance.sol
struct Proposal {
  address proposer;
  bytes32 blobId;
  uint64  deadline;
  uint128 yes;
  uint128 no;
}

function propose(bytes32 blobId, uint64 deadline)
  external returns (uint256 id);
function vote(uint256 id, bool support) external;
function tally(uint256 id) external view
  returns (uint128, uint128, bool passed, bool closed);
proposer
walrus PUT
propose(blobId)
03 · try it locally

Two CLIs, four env vars per CLI, end-to-end in five minutes.

01

Deploy the contract

cd showcases/contracts
forge create src/Governance.sol:Governance \
  --rpc-url $EVM_RPC_URL \
  --private-key $DEPLOYER_PRIVATE_KEY \
  --constructor-args $VOTE_TOKEN_ADDRESS

# local: anvil + a dummy ERC-20 you already control work fine.
02

Install the CLIs

cd showcases/04-dao-proposals
pnpm install
EnvRequired?Purpose
EVM_RPC_URLyesJSON-RPC for the chain holding Governance.
EVM_CHAIN_IDdefault 31337Anvil; mainnet=1, sepolia=11155111.
GOVERNANCE_ADDRESSyesDeployed Governance address.
PROPOSER_PRIVATE_KEYpropose onlySigner for the propose() tx.
WALRUS_PUBLISHERdefault testnetPublic Walrus publisher URL.
WALRUS_AGGREGATORdefault testnetWalrus aggregator URL.
WALRUS_EPOCHSdefault 5Blob retention window (≈14 days at 5 epochs).
03

Submit a proposal — dry run first

# dry-run prints the calldata but skips the EVM tx (the Walrus upload still happens)
pnpm propose --dry-run ./fixtures/sample-proposal.md 2027-01-15T00:00:00Z

# real run
pnpm propose ./fixtures/sample-proposal.md 2027-01-15T00:00:00Z
# → [walrus] PUT publisher/v1/blobs?epochs=5  (1234 bytes)
# → [walrus] blobId (base64url): xT4f...
# → [walrus] blobId (bytes32):  0x...
# → [evm] tx submitted: 0xabc...
# → [evm] tx mined in block 1234 (status=success)
# → [evm] lastProposalId: 1
# → [ok] proposal stored — re-fetch the body with:
#        pnpm tally 1

The deadline accepts either a unix-seconds integer or an ISO-8601 string. The contract stores it as uint64.

04

Read the tally (and the body)

pnpm tally 1
# → proposal #1
#     proposer:  0x...
#     deadline:  1768435200  (2027-01-15T00:00:00.000Z)
#     blobId:    0x...
#     aggregator: https://aggregator.walrus-testnet.walrus.space/v1/blobs/xT4f...
#     yes: 0
#     no:  0
#     closed: false
#     passed: false
#     ---- body (1234 bytes) ----
#     # [Proposal] ...

No signer is required for the read — anyone can verify the body matches the on-chain pointer.

05

(Optional) cast a vote

cast send $GOVERNANCE_ADDRESS "vote(uint256,bool)" 1 true \
  --rpc-url $EVM_RPC_URL \
  --private-key $VOTER_PRIVATE_KEY

# then re-run `pnpm tally 1` and the yes count reflects voteToken.balanceOf(voter).
04 · verify

What "good" looks like.

05 · what this is NOT

Limits to set expectations.

  • Not production governance. The flash-loan attack vector on live balanceOf reads is real. Use ERC20Votes + getPastVotes in any real DAO.
  • Not a Snapshot replacement. No off-chain signature aggregation, no EIP-712 envelope, no delegate weight, no quorum logic. The contract is a demonstration of where the body lives, not a full vote envelope.
  • Not a Walrus pricing oracle. epochs is a number the proposer picks. WAL cost is paid by the proposer's wallet to the publisher upfront — neither the DAO contract nor the voters touch WAL.
  • --dry-run still spends WAL. The Walrus upload runs before the dry-run gate so the CLI can preview the real blobId; only the EVM tx is skipped. Rename to --no-broadcast in your fork if the literal "dry-run = no side effects" reading matters.
06 · source files

Where to look in the repo.