0x434837092605038A8C1230682F72966AbD13f3F5
Overview
EtherForum is a fully decentralized, censorship-resistant public square powered by a single immutable smart contract on Ethereum. There are no servers, no admins, and no off-chain databases. Everything—accounts, posts, comments, reactions, follows, profiles—lives on-chain.
Architecture
Contract: Single Solidity contract (pragma ^0.8.0) implementing users, posts, comments, reactions, and follows.
UI: A minimal web client that reads/writes directly to the contract via a standard provider. It reconstructs timelines via counters and contiguous IDs, batch-loads in slices on scroll, supports deep links (#post{ID} and #post{ID}/#comment{ID}), and refreshes the home feed on an interval.
Why this design is smart (and fast)
- Single source of truth: Ethereum state → zero drift, zero “event lag”.
- Zero ops: No indexer/DB/cron to run or maintain. Fewer moving parts; fewer outages.
- Symmetric clients: Any UI makes the same calls and gets the same answers. No privileged backend.
- Latency bounded by RPC: Batch read-only calls with Multicall to reduce round-trips.
- Forkable by design: Anyone can build a new client without permission; the chain is the API.
Data Model
// Users
struct UserBasic { uint256 userId; string username; uint256 accountCreationTime; uint256 accountCreationBlock; bool isRegistered; }
struct UserProfile { string nickname; string about; string website; string location; string profilePicture; string coverPicture; uint256 pinnedPost; }
struct UserStats { uint256 postCount; uint256 commentCount; uint256 followerCount; uint256 followingCount; }
// Content
enum Reaction { NONE, LIKE, DISLIKE }
struct Post {
uint256 globalPostId; address author; uint256 authorPostId; uint256 postTime;
string content; uint256 commentCount; uint256 likeCount; uint256 dislikeCount; uint256 repostCount;
bool isHidden; bool isRepost; uint256 originalPostId; string reposterContent;
mapping(address => Reaction) reactions;
}
struct Comment {
uint256 commentId; address author; uint256 commentTime; string comment;
uint256 likeCount; uint256 dislikeCount; bool isHidden; mapping(address => Reaction) reactions;
}
// Key indices
uint256 private totalUsers; uint256 private globalPostCount;
mapping(string => address) usernameToAddress; // case-insensitive lookup
mapping(uint256 => address) userAddressById; // userId → address
mapping(address => mapping(uint256 => uint256)) userPostId; // per-user idx → global post id
mapping(uint256 => Post) allPosts; // global posts
mapping(uint256 => mapping(uint256 => Comment)) postComments; // per-post contiguous comments
// UI booleans
mapping(uint256 => mapping(address => bool)) hasCommentedOnPost;
mapping(uint256 => mapping(address => bool)) hasRepostedOnPost;
// Social graph
mapping(address => mapping(address => bool)) isFollowing;
The key: contiguous, monotonic IDs and authoritative counters make reads trivial and deterministic.
Identity & Usernames
- Registration: createAccount(nickname, username) saves UserBasic, UserProfile, and UserStats, and assigns a monotonic userId.
- Case-insensitive uniqueness: Usernames are validated (≥5 chars, lowercase letters/digits). The lowercase form indexes usernameToAddress; display casing is preserved in UserBasic.username.
- Lookups: Resolve by username (getUserAddressByUsername) or by userId (getUserAddressById).
Posts & Reposts
- Creation: createPost(content) increments globalPostCount and the author’s postCount; assigns globalPostId and authorPostId. Text must be non-empty.
- Editing & Hiding: Authors can editPost or hidePost. Hidden posts keep IDs/counters; getters return empty text when hidden.
- Reposts: createRepost(originalId, reposterContent) links to the original and increments its repostCount. editRepost updates reposter note.
- Pinning: pinPost(globalPostId) (or 0 to unpin) is enforced on-chain for the author only.
Reactions & Social
- Reactions: Per-item mapping(address => Reaction) supports constant-time checks. The API accepts "like"/"dislike" strings and adjusts counters on change.
- Follow graph: followUser(target, bool) flips isFollowing and maintains follower/following counters with underflow guards.
- Interaction flags: hasCommentedOnPost and hasRepostedOnPost provide O(1) UI gating.
Visibility & Integrity
- Soft hide: isHidden flags avoid breaking links or pagination; getters return empty text when hidden.
- Repost integrity: Reposts record originalPostId and bump the original’s repostCount.
Query Patterns (Indexer-Free)
Global/Home Feed
// 1) Latest post id
N = getGlobalPostCount()
// 2) Fetch descending in batches
for id in [N, N-1, ..., N-k]: render(getPost(id))
User Feed
// 1) Per-user total
K = getUserStats(user).postCount
// 2) Jump table per-user idx → global id
for i in [K, K-1, ..., K-k]: id = getGlobalPostId(user, i); render(getPost(id))
Comments
// 1) Count
C = getPostCommentCount(postId)
// 2) Contiguous 1..C
for i in [1 .. C]: render(getComment(postId, i))
Instant UI Toggles
liked = getUserReactionOnPost(postId, me)
following = getIsFollowing(me, user)
reposted = getHasRepostedAPost(postId, me)
Tip: Batch read-only calls with Multicall to reduce latency on mobile.
Contract API (Selected)
Accounts
createAccount(string nickname, string username)
changeUsername(string newUsername)
isUserRegistered(address user) → bool
getUserAddressById(uint256 id) → address
getUserAddressByUsername(string username) → address
getUserBasic(address) → (userId, username, createdAt, createdBlock, isRegistered)
getUserProfile(address) → (nickname, about, website, location, profilePicture, coverPicture, pinnedPost)
getUserStats(address) → (postCount, commentCount, followerCount, followingCount)
Posts
createPost(string content)
,editPost(uint256 id, string newContent)
,hidePost(uint256 id, bool)
createRepost(uint256 originalId, string reposterContent)
,editRepost(uint256 id, string newReposterContent)
getGlobalPostCount() → uint256
,getGlobalPostId(address, uint256) → uint256
,getPost(uint256) → (id, author, authorPostId, time, text, commentCount, likeCount, dislikeCount, hidden, isRepost, originalPostId, repostCount)
Comments
createComment(uint256 postId, string text)
,editComment(uint256 postId, uint256 commentId, string newText)
,hideComment(uint256 postId, uint256 commentId, bool)
getPostCommentCount(uint256 postId) → uint256
,getComment(uint256 postId, uint256 commentId) → (...)
getUserComment(address user, uint256 userCommentId) → (postId, commentId)
,getUserCommentCount(address) → uint256
Social & Reactions
followUser(address target, bool follow)
,getIsFollowing(address follower, address followed) → bool
reactToPost(uint256 postId, string reaction)
,reactToComment(uint256 postId, uint256 commentId, string reaction)
getUserReactionOnPost(uint256 postId, address user) → string
,getUserReactionOnComment(uint256 postId, uint256 commentId, address user) → string
getHasCommentedOnPost(uint256 postId, address user) → bool
,getHasRepostedAPost(uint256 postId, address user) → bool
UI Implementation Notes
- Routing & Deep Links: The UI supports
#post{ID}
and#post{ID}/#comment{ID}
routes for single-post and single-comment views; other hashes map to usernames or sections. - Feeds: Home feed walks backward from
getGlobalPostCount()
in batches onwindow.onscroll
. User feed maps per-user indices viagetGlobalPostId(user,i)
. Comments load contiguously usinggetPostCommentCount()
. - Profile/Stats Widgets: “Recently Joined” reads
getTotalUsers()
then iterates downwards to fetch the latest addresses viagetUserAddressById()
. “Platform Stats” readsgetTotalUsers()
andgetGlobalPostCount()
. - Reactions: Buttons call
reactToPost
/reactToComment
. Highlight state is re-derived viagetUserReactionOnPost
/...OnComment
. - Markdown: Post/comment text is parsed with
marked
;@mentions
are auto-linked to#username
. (If building your own client, add an HTML sanitizer after parsing.) - Caching: The UI caches user display names/profile pics per address to reduce repeated calls.
- Auto Refresh: Home feed can auto-reload periodically on the home route.
FAQ
It’s a fully decentralized, adminless social protocol on Ethereum. Accounts, posts, edits, comments, reactions, follows, and profiles all live on-chain.
- Immutable posts and comments
- On-chain edits & hides (soft-hide)
- Reposts with optional notes
- Reactions (like/dislike), pinning, following
- Case-insensitive unique usernames
- Markdown & image embeds
EtherForum’s contract is designed so the blockchain itself is the database. Every post, comment, reaction, and follow is stored as structured state directly in Ethereum — no hidden servers, no off-chain caches.
- Deterministic IDs — Each post gets a predictable ID from global and per-user counters.
- Jump tables & mappings — Timelines are loaded by walking
getGlobalPostCount()
or per-user counters in order. - Pure view reads — Frontends fetch data directly from Ethereum nodes using view functions.
- No DB, no cron — The contract’s state is the source of truth; no privileged backend needed.
- Client freedom — Any developer can build their own frontend or tool without relying on EtherForum.org.
In short: if you can connect to Ethereum, you can read EtherForum — no middlemen, no hidden logic.
No. EtherForum has no central admin, no server-side control, and no privileged keys. The contract is deployed on Ethereum and cannot be modified to censor or delete accounts or posts.
- No admin role — The smart contract contains no functions to forcibly remove or ban users
- Immutable content — All posts, comments, and profile changes are stored in Ethereum’s state and can be verified by anyone
- Multiple clients possible — Even if one frontend disappears, others can instantly read and write to the same contract
- On-chain governance-free — No DAO or vote can override your individual posting rights
As long as the Ethereum network exists, EtherForum’s data and functions will be accessible.
EtherForum does not rely on a single domain or server to operate. The official site is just one interface to the on-chain data. If it goes offline, you still have multiple options:
- Use alternative frontends built by the community
- Run the open-source client locally on your device
- Call the contract directly from any Ethereum wallet or dApp browser
- Query and publish via standard Ethereum JSON-RPC methods
Because all content lives in Ethereum’s state, it is permanent, verifiable, and accessible from anywhere — no matter what happens to the original website.
Registration is done entirely on-chain through the createAccount(nickname, username)
function. When you register, you choose a public username (subject to the username criteria) and a display nickname. Your account is assigned a unique, immutable userId
.
Once registered, you can update your profile at any time. Profile fields are stored fully on-chain and visible to all clients. They include:
- Nickname — A display name separate from your username
- About — A short bio or description
- Website — A link to your personal or project site
- Location — Optional geographical info
- Profile Picture — URL to an image rendered by clients
- Cover Picture — URL to a banner image
- Pinned Post — One post you choose to feature at the top of your profile
Because these fields are on-chain, any frontend can display them exactly as you set them. No centralized service can alter or remove your profile data.
Posting on the platform requires that you have an on-chain account. You must first register using createAccount(nickname, username)
, which assigns you a unique userId
and stores your profile details in the contract.
Once registered, you can publish posts by calling createPost(content)
. The content must be non-empty text and is stored permanently on Ethereum. Every post is given a unique globalPostId
and is linked to your account’s post count.
- Registration Required — Unregistered addresses cannot create posts
- Content Format — Plain text with optional Markdown formatting and image embeds
- Permanent Record — Posts are stored on-chain and can be displayed by any frontend
- No Size Limits by Contract — The only limit is Ethereum transaction gas cost
All posts are immutable except by their author, who can later edit or hide them using editPost
or hidePost
.
Usernames are unique across the entire platform and stored fully on-chain. They are case-insensitive — the system treats uppercase and lowercase as the same (e.g., John
and john
are identical).
Valid usernames must:
- Be at least 5 characters long
- Contain only lowercase letters (
a-z
) and digits (0-9
) - Have no spaces, underscores, hyphens, emojis, or special characters
Example valid username: alex99
Example invalid username: Alex_99!
Yes. You can change your username at any time as long as the new one meets the username criteria and isn’t already taken. When you change it, your old username becomes available for others to claim.
A repost is a way to share someone else’s post on your own timeline. You can optionally add your own note or comment when reposting. The original post’s repost count will increase, and the repost will always be linked to it on-chain.
You can edit your own posts, repost notes, and comments at any time. When editing a post, you can change its content as long as it is not a repost (reposts have their own edit function for the note). Edits update the on-chain content immediately for all viewers.
You can pin one of your posts to your profile so it always appears at the top of your timeline. Pinning does not create a copy — it just tells the UI to display that post first. You can unpin or change your pinned post at any time.
You can hide your own posts and comments. Hidden items remain stored on-chain but the UI will ignore them (showing them as empty or not at all). You can unhide them later if you wish.
followUser(addr, bool)
flips your on-chain follow state and updates counters. The contract stores booleans for relationships (not huge follower lists).
In the future, with the block sync and notifications system, the UI will check for new posts every block and only import them if following = true
for the author.
Yes. Headings, bold/italic, links, lists, quotes, inline/code blocks, and image embeds with 
. Content is stored on-chain as text and rendered by clients.
Examples: **bold**
→ bold, *italic*
→ italic, [link](https://...)
→ link, `inline code`
→ inline code
.
Writing requires standard Ethereum gas. Reading is free via public RPCs.
Comments