/p/moul/x/grc20wrap/v0
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:

🧪 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.