PathrockNetwork Gno Explorer
HomeBlocksTransactionsTokensRealmsPackagesValidatorsAnalytics

PathrockNetwork Gno Explorer — an independent explorer for Gno.land Mainnet (gnoland-1), operated by PathrockNetwork. Not an official Gno.land service.

gnowebarchive RPC

gno.land/p/moul/x/grc20wrap/v0

Package
Open in gnoweb ↗

Overview

Kind
Pure package
Name
v0
Namespace
moul / x / grc20wrap
Files
8 (README)(gnomod.toml)
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

Files (8)

  • README.mdmarkdown
  • gnomod.tomltoml
  • basket.gnogno
  • policy.gnogno
  • wrap.gnogno
  • basket_test.gnogno
  • policy_test.gnogno
  • wrap_test.gnogno
  • wrap.gnogno
    1// Package grc20wrap builds derived GRC20 tokens on top of existing ones.2//3// Two shapes:4//5//   - Vault wraps ONE underlying token. It escrows underlying units at the6//     host realm's address and issues its own GRC20 against that escrow. A7//     Policy sets the exchange rate in both directions and may veto a move of

    Functions

    not supported for pure packages by the node (vm/qfuncs)

    Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.

    8// the wrapped token, which is where a wrapper stops being plumbing and
    9// becomes a pattern: 1:1 custody, a denomination change, a
    10// non-transferable receipt, a share in a growing pool.
    11// - Basket wraps SEVERAL. One meta-token is minted against fixed parts of
    12// every leg and splits back into them on redemption.
    13//
    14// Neither type holds chain state of its own beyond the GRC20 ledger it
    15// creates, so a realm keeps them wherever it likes.
    16//
    17// # How custody works
    18//
    19// Escrow lands at the HOST REALM's address. A vault moves it through
    20// grc20.RealmTeller, which binds eagerly to that address, so a vault can only
    21// ever spend what its own realm holds. Pulling a user's tokens in therefore
    22// takes the ordinary allowance route: the user approves the host realm's
    23// address on the underlying token, through that token realm's own entry
    24// point, and the vault draws on the allowance as itself.
    25//
    26// # Reverting, not returning
    27//
    28// Wrapping is two ledger writes on two different tokens, and gno has no
    29// rollback short of a panic: a returned error does not undo what already ran.
    30// Every method here therefore validates while nothing has moved and returns an
    31// error, then panics if a write fails after the point of no return. Aborting
    32// the transaction is the only atomicity available.
    33//
    34// # What a Policy can and cannot enforce
    35//
    36// A Policy veto binds end users, not realms. Moving the wrapped token through
    37// Vault.Move is the only path a signing account has, because MsgCall cannot
    38// build the realm argument grc20's tellers require. Another realm holding the
    39// wrapped token can always call grc20.RealmTeller on it and move its OWN
    40// balance. Soulbound means "no user can pass it on", not "it can never move".
    41//
    42// Live demo: gno.land/r/moul/x/grc20wrapdemo/v0.
    43package grc20wrap
    44
    45import (
    46 "errors"
    47 "math/overflow"
    48
    49 "gno.land/p/nt/grc20/v0"
    50 "gno.land/p/nt/seqid/v0"
    51 "gno.land/p/nt/ufmt/v0"
    52)
    53
    54var (
    55 ErrNilToken = errors.New("grc20wrap: nil underlying token")
    56 ErrInvalidAmount = errors.New("grc20wrap: amount must be positive")
    57 ErrDust = errors.New("grc20wrap: amount rounds to zero")
    58 ErrUnbacked = errors.New("grc20wrap: payout exceeds this vault's escrow")
    59 ErrSoulbound = errors.New("grc20wrap: soulbound, the wrapped token cannot change hands")
    60 ErrOverflow = errors.New("grc20wrap: arithmetic overflow")
    61 ErrBadRatio = errors.New("grc20wrap: ratio terms must be positive")
    62 ErrBadBPS = errors.New("grc20wrap: basis points must be within 0..10000")
    63 ErrBadLegs = errors.New("grc20wrap: a basket needs at least two legs, each with a positive part")
    64 ErrShortBalance = errors.New("grc20wrap: insufficient balance on a leg")
    65 ErrShortAllowance = errors.New("grc20wrap: insufficient allowance on a leg")
    66)
    67
    68// Vault escrows one underlying GRC20 and issues another against it.
    69//
    70// The escrow account is the host realm's own address, shared with every other
    71// vault that realm creates. Separation between them is the per-vault Held()
    72// counter, not separate accounts: a vault only ever releases what it recorded
    73// taking in, so the realm-wide invariant is sum(Held) <= Custody().
    74type Vault struct {
    75 under *grc20.Token
    76 teller grc20.Teller // bound to home, eagerly, at construction
    77 home address
    78 tok *grc20.Token
    79 led *grc20.PrivateLedger
    80 pol Policy
    81 held int64
    82}
    83
    84// NewVault issues a new GRC20 backed by units of under.
    85//
    86// Call it from the realm that will own the vault, forwarding that realm's own
    87// cur: both the teller binding and the new token's origRealm are taken from it
    88// and cannot be forged afterwards. id disambiguates several tokens minted by
    89// the same realm - allocate it from one persistent seqid.ID.
    90//
    91// A nil pol means OneToOne.
    92func NewVault(under *grc20.Token, pol Policy, name, symbol string, decimals int, id seqid.ID, rlm realm) *Vault {
    93 if under == nil {
    94 panic(ErrNilToken)
    95 }
    96 if pol == nil {
    97 pol = OneToOne{}
    98 }
    99 tok, led := grc20.NewToken(name, symbol, decimals, id, rlm)
    100 return &Vault{
    101 under: under,
    102 teller: under.RealmTeller(0, rlm),
    103 home: rlm.Address(),
    104 tok: tok,
    105 led: led,
    106 pol: pol,
    107 }
    108}
    109
    110// Token is the wrapped token this vault issues. It is safe to hand out: a
    111// *grc20.Token carries metadata and read access, never the authority to debit
    112// anybody.
    113func (v *Vault) Token() *grc20.Token { return v.tok }
    114
    115// Underlying is the token held in escrow.
    116func (v *Vault) Underlying() *grc20.Token { return v.under }
    117
    118// Policy is this vault's personality.
    119func (v *Vault) Policy() Policy { return v.pol }
    120
    121// Home is the escrow account: the host realm's address.
    122func (v *Vault) Home() address { return v.home }
    123
    124// Held is the underlying escrowed for THIS vault, by its own accounting.
    125func (v *Vault) Held() int64 { return v.held }
    126
    127// Supply is the wrapped token in circulation.
    128func (v *Vault) Supply() int64 { return v.tok.TotalSupply() }
    129
    130// Custody is the underlying actually sitting at the escrow account. It covers
    131// every vault the host realm created over the same underlying, plus anything
    132// sent there by mistake, so Custody >= Held is necessary for solvency and
    133// sufficient only when this is the realm's single vault over that token.
    134func (v *Vault) Custody() int64 { return v.under.BalanceOf(v.home) }
    135
    136// Solvent reports whether the escrow account still covers what this vault
    137// recorded taking in. False means the host realm moved the underlying behind
    138// the vault's back.
    139func (v *Vault) Solvent() bool { return v.Custody() >= v.held }
    140
    141// Wrap escrows amount units of the underlying from `from` and mints the
    142// wrapped token to `from`, returning how much was minted.
    143//
    144// `from` must already have granted v.Home() an allowance of at least amount on
    145// the underlying token, set through that token realm's own entry point.
    146func (v *Vault) Wrap(_ int, rlm realm, from address, amount int64) (int64, error) {
    147 if amount <= 0 {
    148 return 0, ErrInvalidAmount
    149 }
    150 out, err := v.pol.WrapRate(v, amount)
    151 if err != nil {
    152 return 0, err
    153 }
    154 if out <= 0 {
    155 return 0, ErrDust
    156 }
    157 // Last point at which nothing has moved.
    158 if err := v.teller.TransferFrom(0, rlm, from, v.home, amount); err != nil {
    159 return 0, err
    160 }
    161 held, ok := overflow.Add64(v.held, amount)
    162 if !ok {
    163 panic(ErrOverflow)
    164 }
    165 v.held = held
    166 if err := v.led.Mint(from, out); err != nil {
    167 // The escrow already moved; only aborting the tx can undo it.
    168 panic(err)
    169 }
    170 return out, nil
    171}
    172
    173// Unwrap burns amount wrapped units held by `from` and releases the underlying
    174// back to it, returning how much was released.
    175func (v *Vault) Unwrap(_ int, rlm realm, from address, amount int64) (int64, error) {
    176 if amount <= 0 {
    177 return 0, ErrInvalidAmount
    178 }
    179 out, err := v.pol.UnwrapRate(v, amount)
    180 if err != nil {
    181 return 0, err
    182 }
    183 if out <= 0 {
    184 return 0, ErrDust
    185 }
    186 if out > v.held {
    187 return 0, ErrUnbacked
    188 }
    189 // Burn first: it is the step that can legitimately fail on a short
    190 // balance, and it fails before anything has moved.
    191 if err := v.led.Burn(from, amount); err != nil {
    192 return 0, err
    193 }
    194 v.held -= out
    195 if err := v.teller.Transfer(0, rlm, from, out); err != nil {
    196 // The burn already happened; only aborting the tx can undo it.
    197 panic(err)
    198 }
    199 return out, nil
    200}
    201
    202// Donate escrows amount units of the underlying without minting anything.
    203//
    204// Under a pool policy this is how the wrapper gets interesting: the donation
    205// raises what every outstanding wrapped unit redeems for, so anyone can make
    206// every holder richer and nobody can take it back out except by holding.
    207// `from` must have approved v.Home() on the underlying, as for Wrap.
    208func (v *Vault) Donate(_ int, rlm realm, from address, amount int64) error {
    209 if amount <= 0 {
    210 return ErrInvalidAmount
    211 }
    212 if err := v.teller.TransferFrom(0, rlm, from, v.home, amount); err != nil {
    213 return err
    214 }
    215 held, ok := overflow.Add64(v.held, amount)
    216 if !ok {
    217 panic(ErrOverflow)
    218 }
    219 v.held = held
    220 return nil
    221}
    222
    223// Move transfers wrapped units after the policy has had its say. It is the
    224// only path a signing user has to move the wrapped token; see the package doc
    225// for what that does and does not guarantee.
    226func (v *Vault) Move(from, to address, amount int64) error {
    227 if amount <= 0 {
    228 return ErrInvalidAmount
    229 }
    230 if err := v.pol.CanMove(v, from, to, amount); err != nil {
    231 return err
    232 }
    233 return v.led.Transfer(from, to, amount)
    234}
    235
    236// Allow sets `spender`'s allowance over `owner`'s wrapped balance.
    237//
    238// The policy is deliberately not consulted: an allowance moves nothing. It is
    239// checked when the allowance is spent, in MoveFrom, so a policy that starts
    240// vetoing later still binds allowances granted before.
    241func (v *Vault) Allow(owner, spender address, amount int64) error {
    242 return v.led.Approve(owner, spender, amount)
    243}
    244
    245// MoveFrom spends `spender`'s allowance over `owner`'s wrapped balance, after
    246// the policy has had its say.
    247func (v *Vault) MoveFrom(spender, owner, to address, amount int64) error {
    248 if amount <= 0 {
    249 return ErrInvalidAmount
    250 }
    251 if err := v.pol.CanMove(v, owner, to, amount); err != nil {
    252 return err
    253 }
    254 return v.led.TransferFrom(owner, spender, to, amount)
    255}
    256
    257// Summary renders the vault as a markdown block.
    258func (v *Vault) Summary() string {
    259 s := ufmt.Sprintf("**%s** (%s) - %s wrapper over %s\n\n",
    260 v.tok.GetName(), v.tok.GetSymbol(), v.pol.Name(), v.under.GetSymbol())
    261 s += ufmt.Sprintf("- wrapped supply: %d\n", v.Supply())
    262 s += ufmt.Sprintf("- escrowed underlying: %d\n", v.held)
    263 s += ufmt.Sprintf("- realm custody: %d\n", v.Custody())
    264 if v.Solvent() {
    265 s += "- solvent: yes\n"
    266 } else {
    267 s += "- solvent: **NO**\n"
    268 }
    269 return s
    270}
    271
    272// mulDiv returns a*b/c, rejecting an overflowing product and a zero divisor.
    273// The division truncates toward zero, which always rounds in the vault's
    274// favour rather than the holder's.
    275func mulDiv(a, b, c int64) (int64, error) {
    276 if c <= 0 {
    277 return 0, ErrBadRatio
    278 }
    279 p, ok := overflow.Mul64(a, b)
    280 if !ok {
    281 return 0, ErrOverflow
    282 }
    283 return p / c, nil
    284}
    285