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

v0 source realm

Package nativeify turns a GRC20 into a native coin, which is wugnot run backwards.

Readme View source

gno.land/r/moul/x/nativeify/v0

wugnot, backwards: escrow a GRC20, issue a native coin against it 1:1.

wugnot takes GNOT, a coin the bank holds, and issues a GRC20 receipt, because a GRC20 can be pulled by a contract and a native coin cannot. This realm runs the other direction, because a native coin can do four things a GRC20 cannot:

  • move with no realm call at all, from any wallet
  • ride the -send envelope of a call, so it can pay for something
  • sit beside GNOT on an account page, with nothing registered anywhere
  • be named as the gas denom by a chain whose genesis says so

What you give up, and it is not small

A native coin cannot be pulled. banker.SendCoins refuses any from that is not the banker's own realm address, so there is no allowance, no TransferFrom, and no way for an AMM, an escrow or a subscription to take what you approved. Everything downstream of Approve stops working the moment a coin becomes native. A test in this package pins it: the realm holds the strongest banker the chain offers, the one that can mint and destroy this denom at will, and it still cannot move a unit it does not already hold.

And the issuer of a native coin can always delete a balance, because RemoveCoin takes an arbitrary address. This realm never calls it on an address other than its own, and it is deployed public rather than private = true so that the code saying so can never be replaced. Those two together are the entire guarantee; there is no on-chain way to prove a negative about code.

Use it

# 1. play money
maketx call -pkgpath gno.land/r/moul/x/grc20faucet/v0 -func Claim

# 2. let this realm pull your RED (Home() prints the address)
maketx call -pkgpath gno.land/r/moul/x/grc20faucet/v0 -func Approve \
  -args RED -args <home> -args 1000000

# 3. create the pair once, then wrap into it
maketx call -pkgpath gno.land/r/moul/x/nativeify/v0 -func Nativeify \
  -args gno.land/r/moul/x/grc20faucet/v0.RED -args nred
maketx call -pkgpath gno.land/r/moul/x/nativeify/v0 -func Wrap -args nred -args 500000

# 4. it is an ordinary coin now
maketx send -to <address> -send 1000/gno.land/r/moul/x/nativeify/v0:nred

# 5. redeem, by sending the coins back
maketx call -pkgpath gno.land/r/moul/x/nativeify/v0 -func Unwrap -args nred \
  -send 1000/gno.land/r/moul/x/nativeify/v0:nred

Step 5 is the interesting one. This realm could delete your coins directly and save you a flag, and it does not: a realm whose normal operation is to remove coins from addresses that never handed them over has no way left to demonstrate that it only ever does so consensually.

The rate is 1, and solvency is checkable without trusting this realm

One smallest unit of the underlying is one unit of the coin. Not 10^decimals: the bank has no notion of divisibility, so a token with 6 decimals produces a coin counted in the token's own smallest units, and the 6 travels to nativereg as a display hint and nowhere else.

Solvency reads the escrow from the GRC20 ledger and the supply from the bank, and consults this realm's own bookkeeping for neither. If the two ever disagree, the disagreement is the answer.

One native denom per underlying token: a second would double the chain's per-denom storage for no new capability, and realm-denom balances live under their own store keys, which are not gas-metered.


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/r/moul/x/nativeify/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.

Overview

Package nativeify turns a GRC20 into a native coin, which is wugnot run backwards.

wugnot takes GNOT, a coin the bank holds, and issues a GRC20 receipt for it, because a GRC20 can be pulled by a contract and a native coin cannot. This realm takes a GRC20, escrows it, and issues a native denom against it 1:1, because a native coin can do four things a GRC20 cannot:

  • move with no realm call at all (`gnokey maketx send`, from any wallet)
  • ride the `-send` envelope of a call, so it can PAY for something
  • sit beside GNOT on an account page, with nothing registered anywhere
  • be named as the gas denom by a chain whose genesis says so

What you give up, and it is not small

A native coin cannot be pulled. `banker.SendCoins` refuses any `from` that is not the banker's own realm address, so there is no allowance, no `TransferFrom`, and no way for an AMM, an escrow or a subscription to take what you approved. Everything downstream of `Approve` stops working the moment a coin becomes native. That is not a gap in this realm; it is the property that makes the wrapping worth doing in the other direction.

And the issuer of a native coin can always delete a balance: `RemoveCoin` takes an arbitrary address. This realm never calls it on an address other than its own, Unwrap explains why in detail, and the gnomod.toml explains why this path is public rather than redeployable. Those three together are the entire guarantee. There is no on-chain way to prove a negative about code.

The exchange rate is 1, always

One smallest unit of the underlying is one unit of the coin. Not 10^decimals: the bank has no notion of divisibility, so a token with 6 decimals wrapped here produces a coin counted in the token's own smallest units, and the 6 is carried to `r/moul/x/nativereg` as a display hint and nowhere else.

Solvency is therefore a plain equality that anybody can check without trusting this realm's bookkeeping: the GRC20 balance at this realm's address equals the total supply of the denom. Solvency computes it from both sides.

Play money to try it on: `r/moul/x/grc20faucet/v0`.

Constants 2

const MinBase, MaxBase

1const (
2	MinBase = 3
3	MaxBase = 16
4)
source

MinBase and MaxBase bound a base denom name. The chain enforces exactly this (`isValidBaseDenom` in the banker stdlib): a lowercase letter, then 2 to 15 more lowercase letters or digits. Validating here rather than letting the banker abort is the difference between a usable error and a stack trace.

const Path

1const Path = "gno.land/r/moul/x/nativeify/v0"
source

Path is this realm's package path, which every denom it issues embeds.

Functions 10

func Bases

Action
1func Bases() []string
source

Bases returns every base name this realm issues, in creation order.

func Denom

Action
1func Denom(base string) string
source

Denom returns the denom this realm issues for a base name.

func IsSolvent

Action
1func IsSolvent(base string) bool
source

IsSolvent reports whether every coin issued for a base name is backed.

It allows escrowed > issued, which is what a donation to this realm's address looks like and cannot hurt anybody, and refuses the reverse.

func Nativeify

crossing Action
1func Nativeify(cur realm, tokenKey, base string) string
source

Nativeify creates the native counterpart of a GRC20 registered in `r/nt/grc20reg`, and returns its denom. It issues nothing: Wrap does that.

`base` is the part of the denom after the colon, yours to choose, unique in this realm and in the chain's charset. One native denom per underlying token: a second one would double the chain's per-denom storage for no new capability, and realm-denom balances live under their own store keys, which are not gas-metered.

func Render

1func Render(path string) string
source

Render lists the pairs. `?<base>` shows one.

func Solvency

Action
1func Solvency(base string) (escrowed, issued int64)
source

Solvency returns what is escrowed and what is issued for a base name. They are read from two different places on the chain, the GRC20 ledger and the bank, and this realm's own bookkeeping is not consulted for either: if they ever disagree, the disagreement is the answer.

func TokenKey

Action
1func TokenKey(base string) string
source

TokenKey returns the grc20reg key of the underlying behind a base name.

func Underlying

Action
1func Underlying(base string) *grc20.Token
source

Underlying returns the underlying token behind a base name, for a caller that wants its name, symbol or decimals.

func Unwrap

crossing Action
1func Unwrap(cur realm, base string) int64
source

Unwrap destroys the native coins attached to this call and releases the same number of underlying GRC20 units to the caller.

Why you have to send the coins rather than let this realm take them

This realm holds a `BankerTypeRealmIssue` banker, and `RemoveCoin` takes an arbitrary address, so it could delete the caller's coins directly and save them a flag. It does not, and the reason is worth stating where somebody copying this will read it: a realm whose normal operation is to remove coins from addresses that did not hand them over has no way left to demonstrate that it only ever does so consensually. Every `RemoveCoin` here names this realm's own address, which anybody can check by reading the source, and the coins get to that address the only way a native coin ever moves, by their holder pushing them.

So: attach them.

Example
1-send <amount>/gno.land/r/moul/x/nativeify/v0:<base>

func Wrap

crossing Action
1func Wrap(cur realm, base string, amount int64) int64
source

Wrap escrows `amount` of the underlying and issues the same number of native coins to the caller.

You must `Approve` this realm's address on the underlying token first, through that token's own realm. Until you do, this realm has no authority over your balance at all, and Wrap fails with "insufficient allowance", which is the system working.

Imports 12

Source Files 5