Kind Pure package
Name v0
Namespace moul / x / grc20wrap
Exported functions n/a — not supported for pure packages by the node (vm/qfuncs)
Module gno.land/p/moul/x/grc20wrap/v0
gno 0.9 policy.gno gno
⧉
1 package grc20wrap 2 3 import "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 not supported for pure packages by the node (vm/qfuncs)
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
// 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.
11 type 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.
30 type OneToOne struct {}
31
32 func (OneToOne) WrapRate(v *Vault, in int64 ) ( int64 , error ) { return in, nil }
33
34 func (OneToOne) UnwrapRate(v *Vault, in int64 ) ( int64 , error ) { return in, nil }
35
36 func (OneToOne) CanMove(v *Vault, from, to address, amount int64 ) error { return nil }
37
38 func (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.
48 type Ratio struct {
49 OneToOne
50 Num int64
51 Den int64
52 }
53
54 func (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
61 func (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
68 func (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.
79 type Soulbound struct {
80 Base Policy
81 }
82
83 func (s Soulbound) base() Policy {
84 if s.Base == nil {
85 return OneToOne{}
86 }
87 return s.Base
88 }
89
90 func (s Soulbound) WrapRate(v *Vault, in int64 ) ( int64 , error ) {
91 return s.base().WrapRate(v, in)
92 }
93
94 func (s Soulbound) UnwrapRate(v *Vault, in int64 ) ( int64 , error ) {
95 return s.base().UnwrapRate(v, in)
96 }
97
98 func (s Soulbound) CanMove(v *Vault, from, to address, amount int64 ) error {
99 return ErrSoulbound
100 }
101
102 func (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.
126 type Pool struct {
127 OneToOne
128 }
129
130 func (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
138 func (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
146 func (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.
156 type Fee struct {
157 Base Policy
158 WrapBPS int64
159 UnwrapBPS int64
160 }
161
162 func (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.
170 func 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
181 func (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
189 func (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
197 func (f Fee) CanMove(v *Vault, from, to address, amount int64 ) error {
198 return f.base().CanMove(v, from, to, amount)
199 }
200
201 func (f Fee) Name() string {
202 return ufmt.Sprintf( "fee(%d/%dbps)/%s" , f.WrapBPS, f.UnwrapBPS, f.base().Name())
203 }
204