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

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}