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

vouch.gno

8.73 Kb · 221 lines
  1// Package vouch is a web of trust other realms can gate on: one address
  2// vouches for another, in writing, optionally with GNOT locked behind it.
  3//
  4// The one call that matters is a plain read:
  5//
  6//	import "gno.land/r/moul/x/social/vouch/v0"
  7//
  8//	func Claim(cur realm) {
  9//		if !vouch.IsTrusted(cur.Previous().Address(), 2) {
 10//			panic("get two people to vouch for you first")
 11//		}
 12//		…
 13//	}
 14//
 15// [IsTrusted] takes no cur realm on purpose. A read with no realm token is
 16// borrowed: gno opens no realm frame for it, so it costs the caller nothing
 17// beyond the read and cannot be tricked into acting as anybody. Nothing here
 18// branches on who is asking, and no read in this realm ever will: the answer
 19// to "is this address trusted" must not depend on who wants to know.
 20//
 21// # What it does, and what it refuses to do
 22//
 23// [Vouch] is payable. Any coins sent with it are locked as a bond on that
 24// vouch, which is the voucher putting their own money where their claim is. A
 25// bond is optional and zero is the ordinary case. [Revoke] takes the vouch
 26// back and credits the bond to the voucher, who collects it with [Withdraw].
 27//
 28// There is no slashing in v0 and that is the interesting half. Slashing needs
 29// an arbiter, somebody who decides a vouch was a lie, and every candidate is
 30// a design question rather than a feature: a DAO vote is a popularity contest,
 31// a challenge market pays whoever is loudest, an oracle is one key that can
 32// confiscate anybody's money. The bond is still worth having without it,
 33// because an illiquid deposit is a cost a hundred throwaway addresses cannot
 34// all pay at once.
 35//
 36// # No token
 37//
 38// This realm issues none, deliberately. A transferable vouch is a bought
 39// reputation, and the moment a vouch can be sold the score stops measuring
 40// what it says it measures. The bond is GNOT: value at risk, without being a
 41// market in trust itself. The README says what would change the answer.
 42package vouch
 43
 44import (
 45	"chain"
 46	"chain/banker"
 47	"chain/runtime"
 48	"strconv"
 49
 50	"gno.land/p/moul/x/envelope/v0"
 51	vo "gno.land/p/moul/x/social/vouch/v0"
 52)
 53
 54// realmPath is this realm's own path, the one its gnomod.toml module line
 55// declares. It is written out rather than read from the frame, because every
 56// read this realm exports is borrowed and would report whichever realm called
 57// it, building every link against somebody else's page.
 58const realmPath = "gno.land/r/moul/x/social/vouch/v0"
 59
 60// denom is the only coin a bond can be posted in.
 61const denom = "ugnot"
 62
 63// BoardSize is how many addresses the index page ranks.
 64const BoardSize = 10
 65
 66// graph holds every vouch and the refund ledger. A redeploy would wipe it
 67// while leaving the bonded coins at this address, which is why this realm is
 68// not private (see gnomod.toml).
 69var graph = vo.NewGraph()
 70
 71// Vouch records that the caller stands behind target, for the stated reason.
 72//
 73// It is payable: coins sent with the call are locked as a bond on this vouch
 74// and are returned by [Revoke], never by anything else. Sending nothing is
 75// fine and is the ordinary case.
 76//
 77// Vouching again for the same address UPDATES the reason and ADDS to the
 78// bond. It does not count twice: the score this realm exists to publish is a
 79// count of people, so a second transaction from the same address buys
 80// precisely nothing. Vouching for yourself is refused.
 81func Vouch(cur realm, target address, reason string) {
 82	who, userCall := caller(cur)
 83	bond := bondOf(userCall)
 84
 85	updated, err := graph.Record(who, target, reason, bond, runtime.ChainHeight())
 86	if err != nil {
 87		panic(err.Error())
 88	}
 89	event := "Vouch"
 90	if updated {
 91		event = "Revouch"
 92	}
 93	chain.Emit(event, "from", who.String(), "for", target.String(),
 94		"bond", strconv.FormatInt(bond, 10))
 95}
 96
 97// Revoke withdraws the caller's vouch for target and credits any bond back to
 98// them. The coins do not move here: call [Withdraw] to collect them.
 99//
100// Splitting it in two is the pull-payment pattern. A realm that sent on revoke
101// would be handing control to the recipient in the middle of its own state
102// change, and a recipient that refuses the coins could make revoking
103// impossible.
104func Revoke(cur realm, target address) int64 {
105	who, _ := caller(cur)
106	refund, err := graph.Revoke(who, target)
107	if err != nil {
108		panic(err.Error())
109	}
110	chain.Emit("Revoke", "from", who.String(), "for", target.String(),
111		"refund", strconv.FormatInt(refund, 10))
112	return refund
113}
114
115// Withdraw pays the caller every bond their revoked vouches freed, and
116// returns the amount sent.
117//
118// The credit is zeroed before the coins leave, so a recipient that calls
119// straight back in finds nothing to take.
120func Withdraw(cur realm) int64 {
121	who, _ := caller(cur)
122	amount, err := graph.Withdraw(who)
123	if err != nil {
124		panic(err.Error())
125	}
126	bnk := banker.NewBanker(banker.BankerTypeRealmSend, cur)
127	bnk.SendCoins(cur.Address(), who, chain.NewCoins(chain.NewCoin(denom, amount)))
128	chain.Emit("Withdraw", "to", who.String(), "amount", strconv.FormatInt(amount, 10))
129	return amount
130}
131
132// IsTrusted reports whether addr is vouched for by at least min distinct
133// addresses. This is the gate, and it is one line at the call site.
134//
135// A min below one is read as one: a gate that lets everybody through is a bug
136// in the caller rather than an answer this realm will agree to.
137//
138// It cannot tell you that the vouchers are distinct people. Two addresses
139// vouching for each other both reach a score of one, which [Mutual] exposes
140// and which is the reason to ask for more than one.
141func IsTrusted(addr address, min int) bool { return graph.IsTrusted(addr, min) }
142
143// ScoreOf is how many distinct addresses vouch for addr.
144func ScoreOf(addr address) int { return graph.ScoreOf(addr) }
145
146// BondedFor is the total ugnot locked on addr by everyone vouching for them.
147func BondedFor(addr address) int64 { return graph.BondedFor(addr) }
148
149// VouchedBy is every address that vouches for addr, sorted.
150func VouchedBy(addr address) []address { return graph.VouchedBy(addr) }
151
152// VouchesOf is every address addr vouches for, sorted.
153func VouchesOf(addr address) []address { return graph.VouchesOf(addr) }
154
155// Mutual reports whether a and b vouch for each other, which is the cheapest
156// sybil shape and therefore worth discounting.
157func Mutual(a, b address) bool { return graph.Mutual(a, b) }
158
159// ReasonFrom is what from wrote about target, or the empty string. It is raw
160// caller text: escape it before rendering it anywhere.
161func ReasonFrom(from, target address) string { return graph.ReasonFrom(from, target) }
162
163// BondFrom is what from locked on target, or zero.
164func BondFrom(from, target address) int64 { return graph.BondFrom(from, target) }
165
166// Count is how many vouches exist.
167func Count() int { return graph.Count() }
168
169// People is how many addresses have at least one vouch for them.
170func People() int { return graph.People() }
171
172// Owed is what addr can collect with [Withdraw].
173func Owed(addr address) int64 { return graph.Owed(addr) }
174
175// TotalBonded is everything locked on vouches that still stand.
176func TotalBonded() int64 { return graph.TotalBonded() }
177
178// TotalOwed is every revoked bond nobody has collected yet. This realm holds
179// TotalBonded plus TotalOwed on behalf of other people.
180func TotalOwed() int64 { return graph.TotalOwed() }
181
182// Badge is the one-line trust mark for addr, for a host realm that wants to
183// show what its gate just read. Like [IsTrusted] it is a borrowed read and
184// takes no realm token.
185func Badge(addr address) string {
186	return vo.Badge(realmPath, addr, graph.ScoreOf(addr), graph.BondedFor(addr))
187}
188
189// bondOf reads the coins attached to this call, and only when the chain
190// credited them to THIS realm.
191//
192// The envelope is the transaction's, not the frame's: a realm that was itself
193// paid and then calls here would report coins sitting at its own address, and
194// crediting them would let it bond money this realm never received. A realm
195// may vouch, with no bond; a realm may not vouch while holding somebody's
196// payment, and saying so loudly beats silently reading the bond as zero.
197//
198// It takes the answer rather than the frame because a realm argument in this
199// package must be named cur, which would make this a crossing function.
200func bondOf(userCall bool) int64 {
201	if userCall {
202		return envelope.Amount(denom)
203	}
204	if !envelope.IsEmpty() {
205		panic("a realm cannot forward a bond: those coins were credited to the realm the user paid, not to this one")
206	}
207	return 0
208}
209
210// caller is the address that called us, checked the one way that is safe,
211// plus whether it reached us as a direct user transaction.
212//
213// Both come from here so the realm reads cur.Previous() in exactly one place,
214// the one guarded by cur.IsCurrent().
215func caller(cur realm) (who address, userCall bool) {
216	if !cur.IsCurrent() {
217		panic("spoofed realm: cur is not the live crossing frame")
218	}
219	prev := cur.Previous()
220	return prev.Address(), prev.IsUserCall()
221}