Search Apps Documentation Source Content File Folder Download Copy Actions Download

points.gno

4.87 Kb · 160 lines
  1// Package memba_points_v1 is a soulbound (non-transferable), no-banker on-chain reputation ledger
  2// for the Memba ecosystem. Addresses accrue non-transferable "Memba Points" (MP) via authorized
  3// awarders (the owner or whitelisted awarder realms); MP is READ by other surfaces for status
  4// tiers, feature gating, and leaderboards.
  5//
  6// SAFETY: there is NO transfer, NO banker, and NO OriginSend anywhere — the entire fund-drain risk
  7// class (the OriginSend/IsUserCall/F-1 family) simply does not apply. The ONLY trust boundary is
  8// who may Award/Revoke (the owner ∪ whitelisted awarder realms); every mint/burn emits an event.
  9// Non-transferability is by construction: there is no Transfer/Send/Approve, so points can never be
 10// bought, sold, or wash-traded — MP is reputation, not money ($MEMBA is the money).
 11package memba_points_v1
 12
 13import (
 14	"chain"
 15	"chain/runtime"
 16
 17	"gno.land/p/nt/avl/v0"
 18	"gno.land/p/nt/ufmt/v0"
 19)
 20
 21// AdminAddress is the samcrew-core deploy multisig (same as the other Memba realms). It inits
 22// `owner`; hand it to the memba_dao executor post-deploy via the 2-step ownership transfer.
 23const AdminAddress = "g1x7k4628w93a7wzdhqc06atzx0v50rnshweuxu0"
 24
 25const (
 26	// MaxAward caps a single Award/Revoke amount — a fat-finger + int64-overflow guard.
 27	MaxAward = int64(1_000_000_000)
 28	maxInt64 = int64(9223372036854775807)
 29)
 30
 31// Account is one address's soulbound points balance.
 32type Account struct {
 33	Points    int64
 34	UpdatedAt int64 // block height of last change
 35}
 36
 37var (
 38	owner        address
 39	pendingOwner address
 40	paused       bool
 41	accounts     = avl.NewTree() // addr string -> *Account
 42	awarders     = avl.NewTree() // awarder realm pkgpath -> bool
 43	totalPoints  int64           // Σ current points across all accounts (O(1))
 44	holderCount  int             // number of accounts with a positive balance
 45)
 46
 47func init() { owner = address(AdminAddress) }
 48
 49func assertOwner(cur realm) {
 50	if !cur.IsCurrent() {
 51		panic("spoofed realm")
 52	}
 53	if cur.Previous().Address() != owner {
 54		panic("unauthorized: owner only")
 55	}
 56}
 57
 58func assertNotPaused() {
 59	if paused {
 60		panic("points awarding is paused")
 61	}
 62}
 63
 64func isAwarder(pkgpath string) bool {
 65	_, ok := awarders.Get(pkgpath)
 66	return ok
 67}
 68
 69// assertAwardAuth is the single trust boundary: only the owner (a direct signer) or a whitelisted
 70// awarder realm may mint/burn points. Anti-sybil lives in the awarder's own verified flow — this
 71// realm only enforces that the source is trusted.
 72func assertAwardAuth(cur realm) {
 73	if !cur.IsCurrent() {
 74		panic("spoofed realm")
 75	}
 76	pr := cur.Previous()
 77	if pr.Address() != owner && !isAwarder(pr.PkgPath()) {
 78		panic("unauthorized: owner or whitelisted awarder realm only")
 79	}
 80}
 81
 82// mustAccount returns the account for `a`, or a fresh zero-value one (not yet stored).
 83func mustAccount(a address) *Account {
 84	v, ok := accounts.Get(a.String())
 85	if !ok {
 86		return &Account{}
 87	}
 88	return v.(*Account)
 89}
 90
 91// Award grants `amount` non-transferable points to `to`. Owner or a whitelisted awarder realm only.
 92// Bounded amount + int64 overflow guard. Emits PointsAwarded (with the awarding source realm).
 93func Award(cur realm, to address, amount int64, reason string) {
 94	assertNotPaused()
 95	assertAwardAuth(cur)
 96	if to == "" {
 97		panic("recipient must be non-empty")
 98	}
 99	if amount <= 0 || amount > MaxAward {
100		panic("amount out of range (1..MaxAward)")
101	}
102	a := mustAccount(to)
103	if a.Points > maxInt64-amount {
104		panic("points overflow")
105	}
106	if totalPoints > maxInt64-amount {
107		panic("total points overflow")
108	}
109	old := a.Points
110	a.Points += amount
111	a.UpdatedAt = runtime.ChainHeight()
112	accounts.Set(to.String(), a)
113	totalPoints += amount
114	if old == 0 {
115		holderCount++
116	}
117	rankApply(to.String(), old, a.Points)
118	chain.Emit("PointsAwarded", "to", to.String(), "amount", itoa64(amount),
119		"reason", reason, "source", cur.Previous().PkgPath())
120}
121
122// Revoke removes up to `amount` points from `from`, clamped at zero (never negative). Owner or a
123// whitelisted awarder realm only — moderation / clawback. A no-op on a zero balance. Emits
124// PointsRevoked with the amount actually removed.
125func Revoke(cur realm, from address, amount int64, reason string) {
126	assertNotPaused()
127	assertAwardAuth(cur)
128	if amount <= 0 || amount > MaxAward {
129		panic("amount out of range (1..MaxAward)")
130	}
131	a := mustAccount(from)
132	removed := amount
133	if removed > a.Points {
134		removed = a.Points
135	}
136	if removed == 0 {
137		return
138	}
139	old := a.Points
140	a.Points -= removed
141	a.UpdatedAt = runtime.ChainHeight()
142	accounts.Set(from.String(), a)
143	totalPoints -= removed
144	if a.Points == 0 {
145		holderCount--
146	}
147	rankApply(from.String(), old, a.Points)
148	chain.Emit("PointsRevoked", "from", from.String(), "amount", itoa64(removed), "reason", reason)
149}
150
151// GetPoints returns an address's current points (0 if unknown). Pure.
152func GetPoints(a string) int64 {
153	v, ok := accounts.Get(a)
154	if !ok {
155		return 0
156	}
157	return v.(*Account).Points
158}
159
160func itoa64(n int64) string { return ufmt.Sprintf("%d", n) }