Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

v0 source realm

Package curated is a list anyone can get onto by locking a deposit, and anyone can try to get somebody else off by ma...

Readme View source

r/moul/x/social/curated

A curated list with skin in the game. Anyone lists an entry by locking a deposit, anyone challenges an entry by matching that deposit with a bond, and after a voting window the loser's money goes to the winner. The deposit does not make an entry good. It makes a bad one expensive to leave standing, which is the only reason a list anybody can write to is worth reading at all.

The engine is p/moul/x/social/curated/v0, a pure package that returns errors and moves no coins. This realm is the chain wiring: the envelope, the banker, the events and Render.

The API

call pays does
Apply(key, url, description) exactly 1 GNOT lists the entry immediately
Challenge(key) exactly that entry's deposit opens a vote for 1000 blocks
Vote(key, keep) nothing one address, one vote, while the window is open
Resolve(key) nothing anyone, after the window: pays the winner
Unlist(key) nothing the owner takes their own entry down, if unchallenged
Withdraw() nothing collects everything credited to the caller

Reads: Get, Count, Listed, IsListed, ChallengeOf, CreditOf.

A key is a slug: lowercase ASCII letters, digits, -, _ and ., starting alphanumeric, at most 64 bytes. A key whose entry was removed is free to apply for again: a challenge that wins removes an entry, it does not burn the name.

A majority of keep votes keeps the entry and credits its owner the challenger's bond. Otherwise the entry is removed and the challenger is credited the bond plus the deposit. A tie keeps the entry, including the tie of nobody voting at all: the incumbent paid first and is already at risk, so the burden is on the challenger to produce a reason. If ties went the other way, a challenge that convinced nobody would still win and listing anything would be pointless.

The trap it avoids

Nothing is ever sent to you. Every payout is a credit in an internal ledger, and the payee calls Withdraw() for their own. A realm that looped over winners and sent to each one would fail entirely when one of them could not be paid, and would hand a griefer a denial of service for the price of one entry. Withdraw zeroes the credit before the coins move, so a reentrant call finds nothing left to take.

The consequence is an invariant worth knowing: every ugnot at this realm's address is either backing a live entry or an open challenge, or already assigned to somebody. The tests assert exactly that, against the chain balance rather than against the realm's own books.

Voting is sybil-prone, deliberately and visibly

Vote is one address, one vote, unweighted, and an address is free. A resolution says "nobody with a stake objected enough", never "this is true". Deciding who counts as a person is a different problem with its own realm behind it, r/moul/x/social/vouch, a sibling in this family; until a vote is gated on a vouched identity, the two addresses with money on the outcome are the only ones whose vote means anything.

Which is the second thing: voters are paid nothing in v0. Voting costs gas and returns nothing, so there is no reason for a disinterested address to show up at all. A share of the loser's stake for the winning side is the standard answer, and it is the first thing this realm should grow. After that, an application period, so a bad entry is not visible before anybody can object to it.

Why the bonds are GNOT and not a token of this list's own

A bond has to be denominated in something the challenger already holds. A list that minted its own token and demanded it as the bond would be asking a newcomer to acquire a token whose only use is challenging entries on a list nobody reads yet, which is the chicken-and-egg problem in its purest form: the token is worth something once the list is worth gaming, and the list cannot become worth gaming until challenges work.

So v0 bonds are native GNOT, which every account on the chain already has.

The answer becomes yes under one condition: the list is valuable enough that being on it is contested, and the challenge flow is busy enough that a bond denominated in a list token would have a real market price. Concretely, a steady stream of challenges from addresses that are not the two parties, and a reason to hold the token between challenges. At that point the token earns its own job, and it can pay the voters the paragraph above says go unpaid, which is the sink a bond alone does not provide.

The slot it would drop into is p/moul/x/social/coin/v0, a GRC20 in this family that refuses to exist until its mint rule, its sink and its buyer are all declared. This realm deliberately does not import it: the three answers are not available yet, and a token issued before they are is a token with no reason to be held.


Part of moul/gno-contracts — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage.

On mainnet: deployment status transactions unique callers deployed revision

Dependency graph:

gno.land/r/moul/x/social/curated/v0 dependency graph

🧪 Highly experimental — potentially vibe-coded. Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: DISCLAIMER.

Overview

Package curated is a list anyone can get onto by locking a deposit, and anyone can try to get somebody else off by matching that deposit with a bond. The loser of the challenge pays the winner.

It is the registry member of a family of small social apps: a list is the thing every other one of them eventually needs, and a list is only worth reading if being on it cost something.

Example
1Apply(key, url, description)  -send 1000000ugnot   list it, immediately
2Challenge(key)                -send <the deposit>  object to it
3Vote(key, keep)                                    while the window is open
4Resolve(key)                                       anyone, after it closes
5Unlist(key)                                        the owner, if unchallenged
6Withdraw()                                         collect what you won

Money in, money out

Coins arrive in the envelope of a call and sit at this realm's address. They never leave by being pushed: a payout is a credit in a ledger and the payee calls Withdraw for their own. A realm that looped over winners and sent to each one would fail entirely when one of them could not be paid, and would hand a griefer a cheap denial of service for the price of one entry.

Every ugnot here is therefore either backing a live entry or an open challenge, or already assigned to somebody. The realm's own tests assert exactly that against the chain balance.

Voting is sybil-prone, and the bonds are GNOT

Vote is one address, one vote, unweighted, and an address is free. Read a resolution as "nobody with a stake objected enough", never as a verdict. Gating a vote on something harder to manufacture is a different problem with its own realm behind it, r/moul/x/social/vouch, a sibling in this family.

Bonds are GNOT and not a token of this list's own: see the README on why that is the only answer available at v0 and what would change it.

What v0 does not do, in the order it should be fixed

  1. Voters are paid nothing. Voting costs gas and returns nothing, so the only two addresses with a reason to vote are the ones with money on the outcome. A share of the loser's stake for the winning side is the standard answer and the first thing to add.
  2. There is no application period: Apply lists immediately.
  3. A challenge cannot be withdrawn and a vote cannot be changed.

Constants 1

const Denom, Deposit, ChallengeBlocks

 1const (
 2	// Denom is what a deposit and a bond are paid in. Native coins, so the
 3	// amounts are real to a challenger before this list is worth anything.
 4	Denom = "ugnot"
 5
 6	// Deposit is what listing costs, and therefore also what challenging one
 7	// of today's entries costs. One GNOT: enough to make a thousand junk
 8	// entries a real expense, cheap enough that one honest entry is not a
 9	// decision.
10	Deposit = int64(1000000)
11
12	// ChallengeBlocks is how long a challenge takes to resolve. Long enough
13	// for a reader who is not watching the chain to notice and vote, short
14	// enough that an entry is not held hostage.
15	ChallengeBlocks = int64(1000)
16)
source

Functions 11

func Apply

crossing Action
1func Apply(cur realm, key, url, description string)
source

Apply lists an entry under key, in exchange for exactly Deposit ugnot.

The entry is on the list the moment this returns: there is no application period in v0, and the check on a bad entry is that anybody can challenge it. The deposit comes back through Unlist and Withdraw if nobody ever does.

key is a slug: lowercase ASCII letters, digits, '-', '_' and '.', starting alphanumeric. A key whose entry was removed is free to apply for again.

func Challenge

crossing Action
1func Challenge(cur realm, key string)
source

Challenge objects to an entry, in exchange for a bond equal to that entry's own deposit, and opens a vote that closes ChallengeBlocks blocks later.

An owner cannot challenge their own entry: it would cost them nothing and would make the entry immune to a real challenge for the whole window.

func Count

Action
1func Count() int
source

Count is how many entries are on the list right now. A challenged entry counts: a challenge is an objection, not a verdict.

func IsListed

Action
1func IsListed(key string) bool
source

IsListed reports whether key is on the list right now.

func Render

1func Render(path string) string
source

Render routes two views: the list itself, and one entry.

A key never contains a slash, so "entry/<key>" is a prefix strip rather than a path element that would have to be rejoined.

func Resolve

crossing Action
1func Resolve(cur realm, key string)
source

Resolve closes a challenge whose window has passed. Anyone may call it, so neither party can stall the other by sitting still.

A majority of keep votes keeps the entry and credits its owner the bond. Otherwise the entry is removed and the challenger is credited the bond plus the deposit. A tie, including nobody voting at all, keeps the entry: the incumbent is the one already at risk, so a challenge that convinced nobody loses, which is what makes being wrong cost something.

func Unlist

crossing Action
1func Unlist(cur realm, key string)
source

Unlist takes the caller's own entry off the list and credits them the deposit back, which Withdraw then pays out.

It is refused while a challenge is open: an owner who could walk away mid-challenge would be risking nothing, which is the one thing the deposit exists to prevent.

func Vote

crossing Action
1func Vote(cur realm, key string, keep bool)
source

Vote takes a side on an open challenge: keep the entry, or remove it.

One address, one vote, unweighted, and it cannot be changed. An address is free, so this is cheap to manufacture; see the package doc.

func Withdraw

crossing Action
1func Withdraw(cur realm) int64
source

Withdraw pays the caller everything credited to them and returns it.

This is the only way coins leave the realm. The credit is zeroed before the transfer, so a reentrant call finds nothing left to take.

Imports 12

Source Files 5