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

/p/moul/x/grc20wrap/v0

Directory · 8 Files
README.md Open

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

Build a new GRC20 on top of one you do not control. Vault escrows an existing token and issues its own against it; Basket does the same over several at once. A Policy decides the exchange rate in both directions and whether the wrapped token may move, which is the whole personality of a wrapper.

 1import (
 2    "gno.land/p/moul/x/grc20wrap/v0"
 3    "gno.land/p/nt/grc20/v0"
 4)
 5
 6// In your realm, over any *grc20.Token you can reach (an import, or a
 7// gno.land/r/nt/grc20reg/v0 lookup):
 8v := grc20wrap.NewVault(under, grc20wrap.Pool{}, "Pooled FOO", "pFOO", 4, 0, cur)
 9
10// The holder approves YOUR realm's address on the underlying first, through
11// the underlying realm's own entry point. Then:
12shares, err := v.Wrap(0, cur, holder, 1000)   // escrow 1000, mint shares
13out, err := v.Unwrap(0, cur, holder, shares)  // burn shares, release escrow
14err = v.Donate(0, cur, patron, 500)           // no mint: every share is worth more

The five policies

policy wrapped token behaves like
OneToOne a 1:1 custody receipt
Ratio{Num, Den} the same token re-denominated ({1000, 1} adds three decimals)
Soulbound{Base} a badge: wrap and unwrap freely, never transferable
Pool a share of the escrow; Donate pays every holder at once
Fee{Base, WrapBPS, UnwrapBPS} a haircut left behind, which over Pool pays whoever stays

They compose: Fee{Base: Pool{}, UnwrapBPS: 100} is a pool with a 1% exit fee. Writing your own means three methods and a name; embed OneToOne and override only what differs.

Basket: one token backed by two

1b := grc20wrap.NewBasket(
2    []*grc20.Token{red, blue}, []int64{1, 2},
3    "Purple", "PURPLE", 4, 0, cur,
4)
5err := b.Fuse(0, cur, holder, 100)   // escrow 100 RED + 200 BLUE, mint 100 PURPLE
6err = b.Defuse(0, cur, holder, 40)   // burn 40, hand back 40 RED + 80 BLUE

Proportions are fixed, and the meta-token is only ever minted against the real thing, so it cannot drift from its backing or be arbitraged. It has no price oracle and no rebalancing, because both would mean valuing the legs.

Three things worth knowing before using it

Custody is an allowance, never a privilege. Escrow sits at the host realm's address and moves through grc20.RealmTeller, which grc20 binds eagerly to that address. A wrapper can only take what a holder approved for it, and can only ever spend its own realm's balance.

Errors are returned before anything moves, panics after. Wrapping is two ledger writes on two different tokens and gno has no rollback short of a panic, so each method validates first and returns an error while the world is still untouched, then panics if a write fails past the point of no return. Aborting the transaction is the only atomicity available.

A policy veto binds users, not realms. Vault.Move is the only path a signing account has, because MsgCall cannot build the realm argument grc20's tellers require. A realm holding the wrapped token can always call grc20.RealmTeller and move its own balance. Soulbound means "no user can pass it on", not "it can never move".

Live demo: r/moul/x/grc20wrapdemo · render it at /r/moul/x/grc20wrapdemo/v0.


Part of moul/gno-contracts — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage.

Dependency graph:

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

🧪 Highly experimental — potentially vibe-coded. Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: DISCLAIMER.