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

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}