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

README.md

4.03 Kb · 91 lines

GNOT faucet

A two-step, on-the-record way to get a small amount of GNOT to someone who has none: anyone files a request on their behalf, an approver releases it, and both halves stay on the page with a reason attached.

It exists because the accounts that most need a first coin are exactly the ones that cannot ask for it. An empty account cannot pay the gas to call anything, so Request takes the recipient as an argument and the filer pays for the ask.

This is not the grant board. The grant board weighs work against milestones and proofs; this hands over pocket change so somebody can try the chain at all.

The float

The faucet spends only what has been sent to its own package address, never the approver's balance. Top it up with a plain bank send to that address, or with Fund if you want the donation on the record. Withdraw takes it back, to the owner and nowhere else.

The two calls, and why they travel together

call who what it does
Request(to, amount, reason) anyone files an ask, returns its id, moves no money
Approve(id, to, amount) an approver pays it out of the float
Deny(id, why) an approver closes it unpaid, with the reason on the page
Fund() anyone, payable credits the float and emits an event
Withdraw(amount) the owner returns float to the owner
AddApprover / RemoveApprover / SetMaxPerRequest the owner the knobs

A tm2 transaction carries a list of messages, runs them in order, and stops at the first failure; a failed transaction writes none of their state, only the fee and the sequence survive. So Request and Approve sent as two messages of one transaction either both happen or neither does, and the common case is a single signature.

The id the second message cannot know

Approve names a request by id, and message 2 of a transaction cannot read what message 1 returned. The caller reads NextID before signing and writes that number into the approval, which is a race: another request landing in between shifts the id, and the approval would pay a stranger.

That is why Approve also takes the recipient and the amount it believes it is approving, and aborts when the stored request disagrees. The race then costs a failed transaction instead of the wrong person's money.

Calls

 1# Ask, on someone else's behalf. Amount is in ugnot.
 2gnokey maketx call -pkgpath "gno.land/r/moul/faucet/v0" -func Request \
 3  -args "g1..." -args 100000000 -args "no gas, wants to try the chain" \
 4  -gas-fee 1000000ugnot -gas-wanted 3000000 -broadcast -chainid gnoland-1 moul
 5
 6# What id the next request will get, so an approval can be signed alongside it.
 7gnokey query vm/qeval -data 'gno.land/r/moul/faucet/v0.NextID()'
 8
 9# What the float holds.
10gnokey query vm/qeval -data 'gno.land/r/moul/faucet/v0.Balance()'
11
12# The page.
13gnokey query vm/qrender -data 'gno.land/r/moul/faucet/v0:'

Reading it

Render("") is the board: float, open requests, decided ones, approvers. Render("req/<id>") is one request, with its reason and, if it was refused, why.

Every caller-supplied string on those pages is escaped before it is rendered. A reason is arbitrary text from an arbitrary account and Render output is markdown that gnoweb parses, so an unescaped one can plant a link, an image beacon or page chrome on a page the realm signs for. This path is permanent, so that bug could only ever be fixed at a new path.


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

Dependency graph:

gno.land/r/moul/faucet/v0 dependency graph

⚠️ Disclaimer: provided as-is, without warranty; not security-audited. Full disclaimer: DISCLAIMER.