policy.gno
6.40 Kb · 203 lines
1package grc20wrap
2
3import "gno.land/p/nt/ufmt/v0"
4
5// Policy is a wrapper's personality. It sets the exchange rate in both
6// directions and can veto a move of the wrapped token.
7//
8// Every method receives the vault, so a policy can price against live state -
9// escrow, supply, custody - instead of a frozen constant. Embed OneToOne to
10// write a policy that only overrides what it cares about.
11type Policy interface {
12 // WrapRate returns how many wrapped units `in` underlying units mint.
13 // It is called BEFORE the escrow moves, so v.Held() excludes `in`.
14 WrapRate(v *Vault, in int64) (int64, error)
15
16 // UnwrapRate returns how many underlying units `in` wrapped units
17 // release. It is called before the burn, so v.Supply() includes `in`.
18 UnwrapRate(v *Vault, in int64) (int64, error)
19
20 // CanMove vetoes a transfer of the wrapped token between two accounts.
21 CanMove(v *Vault, from, to address, amount int64) error
22
23 // Name labels the policy in a catalogue. Short, lowercase.
24 Name() string
25}
26
27// OneToOne is the boring wrapper: one underlying unit in, one wrapped unit
28// out, and no restriction on who may hold the result. Useful on its own as a
29// pure custody receipt, and as the embedded base of every policy below.
30type OneToOne struct{}
31
32func (OneToOne) WrapRate(v *Vault, in int64) (int64, error) { return in, nil }
33
34func (OneToOne) UnwrapRate(v *Vault, in int64) (int64, error) { return in, nil }
35
36func (OneToOne) CanMove(v *Vault, from, to address, amount int64) error { return nil }
37
38func (OneToOne) Name() string { return "1:1" }
39
40// Ratio re-denominates: Num wrapped units per Den underlying units. Both terms
41// must be positive.
42//
43// Ratio{Num: 1000, Den: 1} turns one underlying unit into a thousand wrapped
44// ones, which is how you give a token three more decimals without touching the
45// realm that issued it. The reverse direction divides, so a redemption that
46// rounds to zero is refused (ErrDust) rather than silently burning value: with
47// Num=1000, unwrapping 999 wrapped units returns nothing and is rejected.
48type Ratio struct {
49 OneToOne
50 Num int64
51 Den int64
52}
53
54func (r Ratio) WrapRate(v *Vault, in int64) (int64, error) {
55 if r.Num <= 0 || r.Den <= 0 {
56 return 0, ErrBadRatio
57 }
58 return mulDiv(in, r.Num, r.Den)
59}
60
61func (r Ratio) UnwrapRate(v *Vault, in int64) (int64, error) {
62 if r.Num <= 0 || r.Den <= 0 {
63 return 0, ErrBadRatio
64 }
65 return mulDiv(in, r.Den, r.Num)
66}
67
68func (r Ratio) Name() string { return ufmt.Sprintf("%d:%d", r.Num, r.Den) }
69
70// Soulbound wraps another policy and refuses every transfer of the wrapped
71// token. Rates are whatever Base says; a nil Base means OneToOne.
72//
73// The result is an account-bound receipt: anyone can wrap into it and anyone
74// can unwrap back out, but the wrapped unit itself never changes hands. That
75// makes it a membership badge, a proof of deposit, or a vote weight that
76// cannot be rented - while the underlying stays fully liquid one step away.
77//
78// The veto binds signing users only; see the package doc.
79type Soulbound struct {
80 Base Policy
81}
82
83func (s Soulbound) base() Policy {
84 if s.Base == nil {
85 return OneToOne{}
86 }
87 return s.Base
88}
89
90func (s Soulbound) WrapRate(v *Vault, in int64) (int64, error) {
91 return s.base().WrapRate(v, in)
92}
93
94func (s Soulbound) UnwrapRate(v *Vault, in int64) (int64, error) {
95 return s.base().UnwrapRate(v, in)
96}
97
98func (s Soulbound) CanMove(v *Vault, from, to address, amount int64) error {
99 return ErrSoulbound
100}
101
102func (s Soulbound) Name() string { return "soulbound/" + s.base().Name() }
103
104// Pool prices in shares instead of units: the wrapped token is a claim on a
105// fraction of the escrow rather than on a fixed amount.
106//
107// wrap: shares = in * supply / held (1:1 while the pool is empty)
108// unwrap: units = in * held / supply
109//
110// Nothing changes until someone calls Vault.Donate, which adds escrow without
111// minting shares. Every outstanding share is then worth more, permanently and
112// for everyone at once. That is the whole yield-bearing-token pattern in two
113// lines of arithmetic: a fee sink, a staking reward, an airdrop to holders, all
114// the same operation.
115//
116// Two honest caveats:
117//
118// - Division truncates, so a share is always worth marginally less than its
119// exact fraction. The remainder stays in the pool, which is the safe
120// direction: a vault can never promise more than it holds.
121// - A first depositor can wrap one unit, donate a large amount, and make
122// every later deposit smaller than one share round to zero in their favour
123// (the ERC-4626 inflation attack). Seed the pool at creation, or keep the
124// wrapped token's decimals well above the underlying's, before using this
125// anywhere real.
126type Pool struct {
127 OneToOne
128}
129
130func (Pool) WrapRate(v *Vault, in int64) (int64, error) {
131 supply, held := v.Supply(), v.Held()
132 if supply == 0 || held == 0 {
133 return in, nil
134 }
135 return mulDiv(in, supply, held)
136}
137
138func (Pool) UnwrapRate(v *Vault, in int64) (int64, error) {
139 supply := v.Supply()
140 if supply == 0 {
141 return 0, ErrDust
142 }
143 return mulDiv(in, v.Held(), supply)
144}
145
146func (Pool) Name() string { return "pool" }
147
148// Fee takes a basis-point haircut in both directions on top of Base (nil means
149// OneToOne). 100 BPS is 1%.
150//
151// The haircut is not paid to anyone: it stays escrowed. Over a plain base that
152// only strands value, so Fee is meant to sit over Pool, where the stranded
153// units raise what every remaining share redeems for. The wrapper then pays its
154// holders out of its own turnover, and paying twice - in and out - costs more
155// than holding.
156type Fee struct {
157 Base Policy
158 WrapBPS int64
159 UnwrapBPS int64
160}
161
162func (f Fee) base() Policy {
163 if f.Base == nil {
164 return OneToOne{}
165 }
166 return f.Base
167}
168
169// cut removes bps basis points from amount.
170func cut(amount, bps int64) (int64, error) {
171 if bps < 0 || bps > 10000 {
172 return 0, ErrBadBPS
173 }
174 taken, err := mulDiv(amount, bps, 10000)
175 if err != nil {
176 return 0, err
177 }
178 return amount - taken, nil
179}
180
181func (f Fee) WrapRate(v *Vault, in int64) (int64, error) {
182 out, err := f.base().WrapRate(v, in)
183 if err != nil {
184 return 0, err
185 }
186 return cut(out, f.WrapBPS)
187}
188
189func (f Fee) UnwrapRate(v *Vault, in int64) (int64, error) {
190 out, err := f.base().UnwrapRate(v, in)
191 if err != nil {
192 return 0, err
193 }
194 return cut(out, f.UnwrapBPS)
195}
196
197func (f Fee) CanMove(v *Vault, from, to address, amount int64) error {
198 return f.base().CanMove(v, from, to, amount)
199}
200
201func (f Fee) Name() string {
202 return ufmt.Sprintf("fee(%d/%dbps)/%s", f.WrapBPS, f.UnwrapBPS, f.base().Name())
203}