fee.gno
4.25 Kb · 140 lines
1package nsmarket
2
3import (
4 "strconv"
5 "strings"
6
7 "gno.land/r/nym-mnsrehearsal001/nsdata/v2"
8)
9
10// The house fee, expressed the same way the pricing rules are so there
11// is one convention on this project and not two:
12//
13// "1000000" a flat amount in ugnot — 1 GNOT
14// "%3" three percent of the sale
15// "%2.5" two and a half percent; one decimal place is honoured
16//
17// Stored in the vault as a global so it can be changed without
18// redeploying anything, read fresh on every sale so a change takes
19// effect immediately rather than at the next deploy.
20const (
21 keyFee = "marketfee"
22 defaultFee = "1000000" // 1 GNOT
23
24 // A flat fee is a percentage in disguise on a cheap name: 1 GNOT on
25 // a 0.5 GNOT sale is 200%. Rather than forbid cheap listings or let
26 // a seller be charged more than they earn, the fee is capped at half
27 // the sale. The listing screen shows the seller the actual number
28 // before they sign, which is the part that matters.
29 maxFeeBps = int64(5000) // 50%
30 bpsDenom = int64(10000)
31)
32
33func FeeSpec() string {
34 if v := nsdata.GetGlobal(keyFee); v != "" {
35 return v
36 }
37 return defaultFee
38}
39
40// SetFee changes it. Admin only, and validated here rather than trusted,
41// because a malformed spec would otherwise be read on every sale and
42// silently resolve to zero.
43/*
44SetFee CANNOT WORK FROM HERE, and saying so is the fix.
45
46nsdata.SetGlobalRecord opens with assertIsTrustedLogic, and this realm is
47deliberately outside the trusted set — types.gno says so in capitals, and
48the deploy panel actively revokes anything but nslogic. So every call
49here passed its own admin check and its own validation and then panicked
50inside the vault, for everybody, including the correct admin. A dead
51control that reads as live is worse than no control: it fails at signing
52time, during the incident that prompted reaching for it.
53
54Granting nsmarket vault trust to fix this would surrender the property
55the whole realm is built around. So the setter moved to nslogic, which
56already has the grant, and runs the same parseFee validation there.
57*/
58func SetFee(cur realm, spec string) {
59 panic("nsmarket: fees are set with nslogic.SetMarketFee — this realm has no vault privileges and never did")
60}
61
62// parseFee returns (flat ugnot, basis points, error). Exactly one of the
63// first two is non-zero.
64func parseFee(spec string) (int64, int64, string) {
65 spec = strings.TrimSpace(spec)
66 if spec == "" {
67 return 0, 0, ""
68 }
69 if !strings.HasPrefix(spec, "%") {
70 n, err := strconv.ParseInt(spec, 10, 64)
71 if err != nil || n < 0 {
72 return 0, 0, "fee must be an amount in ugnot or a percentage like %2.5 — got " + spec
73 }
74 return n, 0, ""
75 }
76
77 // A percentage, carried as basis points so "%2.5" is expressible
78 // without floating point anywhere near money.
79 body := spec[1:]
80 whole, frac := body, ""
81 if dot := strings.Index(body, "."); dot >= 0 {
82 whole, frac = body[:dot], body[dot+1:]
83 }
84 if whole == "" {
85 whole = "0"
86 }
87 w, err := strconv.ParseInt(whole, 10, 64)
88 if err != nil || w < 0 {
89 return 0, 0, "bad percentage — " + spec
90 }
91 bps := w * 100
92 if frac != "" {
93 if len(frac) > 1 {
94 return 0, 0, "one decimal place at most — " + spec
95 }
96 f, err2 := strconv.ParseInt(frac, 10, 64)
97 if err2 != nil || f < 0 {
98 return 0, 0, "bad percentage — " + spec
99 }
100 bps += f * 10
101 }
102 if bps > maxFeeBps {
103 return 0, 0, "a fee above 50% is not a marketplace — " + spec
104 }
105 return 0, bps, ""
106}
107
108// FeeFor is what the house takes on a sale at this price, and what the
109// seller is left with. Exported so the listing screen can show both
110// before anybody signs — a fee somebody discovers afterwards is a fee
111// they were not told about.
112func FeeFor(price int64) (fee, toSeller int64) {
113 if price <= 0 {
114 return 0, 0
115 }
116 flat, bps, err := parseFee(FeeSpec())
117 if err != "" {
118 return 0, price // an unreadable spec charges nothing; it never overcharges
119 }
120 if bps > 0 {
121 fee = price * bps / bpsDenom
122 } else {
123 fee = flat
124 }
125 // The cap, applied last, so it bounds a flat fee and a percentage
126 // alike however the spec was written.
127 if ceiling := price * maxFeeBps / bpsDenom; fee > ceiling {
128 fee = ceiling
129 }
130 if fee < 0 {
131 fee = 0
132 }
133 return fee, price - fee
134}
135
136func assertIsAdmin(cur realm) {
137 if cur.Previous().Address() != nsdata.GetAdmin() {
138 panic("nsmarket: restricted to project admin")
139 }
140}