Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}