On gno.land every byte of realm state locks GNOT, and the lock is refunded to
whoever signs the transaction that frees the byte. Deleting state is therefore
paid work. Whether a particular deletion pays depends on two prices that move
independently: the storage price, a chain parameter, and the gas price of the
day. A contract cannot know the second one, so it cannot decide on its own
behalf whether to compact, reindex or reap. It can only publish the size of the
prize and let a caller do the arithmetic.
This package is that arithmetic: pure integer maths, no chain imports, so a
realm can call it inside a Render and an off-chain bot can reuse the identical
formula.
The one number to remember
One byte freed refunds 100 ugnot. At the lowest gas price mainnet has actually
accepted, one ugnot buys 1000 gas. So a byte is worth 100,000 gas, and any
deletion costing less than that per byte pays for itself.
the core arithmetic. BreakEvenBytes rounds up, so a quoted threshold always covers the fee
GasFee · FloorGasFee
a fee from a gas ceiling and a rational gas price. The fee tracks gas_wanted, not gas_used, so unused headroom is paid for
Evaluate · EvaluateAtFloor · Quote
a whole verdict for one candidate cleanup, with a String() fit for a Render
EstimateBytes
what a payload really costs once a realm has wrapped it in an object, at the measured 1.85x
FormatGNOT
ugnot as readable GNOT, because gno has no floats and a bounty quoted in ugnot is unreadable
Two honesty notes
DefaultStoragePrice is a default, not a fact.vm:p:storage_price is
governance settable. Read it from the chain when real money depends on the
answer; the constant is for sizing and display.
EstimateBytes is an estimate. It exists so a bounty shown on a page is
within a factor of two instead of reporting raw payload length. No stdlib call
exposes a realm's real locked storage, so the authoritative numbers are the
chain's: the vm/qstorage query, or the StorageDepositEvent and
StorageUnlockEvent every transaction emits. Never settle an accounting
question with a guess.
v0 formatted by hand with amount = -amount, which leaves math.MinInt64
negative, so both the whole and the fractional part then carried their own sign:
FormatGNOT(math.MinInt64) returned --9223372036854.-775808 GNOT. No caller
in this repo can reach a negative (q.Net is the only one that can be, and it
is printed only inside if q.Worth()), but the function is exported and a
formatter that can return a non-number is not one. v0 stays resolvable.
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.
Overview
Package storagecost answers one question: is it worth paying gas to delete on-chain state?
On gno.land every byte of realm state locks GNOT, and the lock is refunded to whoever signs the transaction that frees the byte. Deleting state is therefore paid work, and whether a given deletion pays depends on two prices that move independently: the storage price, a chain parameter, and the gas price of the day. A contract cannot know the second one, so it cannot decide on its own behalf whether to compact, reindex or reap. It can only publish the size of the prize and let a caller do the arithmetic.
This package is that arithmetic. It is pure integer maths with no chain imports, so a realm can call it inside a Render and an off-chain bot can reuse the identical formula.
The single fact worth remembering is the ratio. One byte freed refunds 100 ugnot, and at the lowest gas price mainnet has actually accepted one ugnot buys 1000 gas. So a byte is worth 100,000 gas, and any deletion costing less than that per byte pays for itself.
Demo realms: gno.land/r/moul/x/reaper for reaping expired state, and gno.land/r/moul/x/compact for the compaction half.
1const( 2// DefaultStoragePrice is the ugnot locked per byte of realm state, the 3// default of the chain's vm:p:storage_price parameter. It is governance 4// settable, so read it from the chain rather than trusting this constant 5// when real money depends on the answer. 6DefaultStoragePriceint64=100 7 8// GasPerUgnotFloor is how much gas one ugnot buys at the lowest gas price 9// mainnet has been observed to accept, 0.001 ugnot per gas. It is a floor,10// not a promise: the fee a node requires tracks gas_wanted, so asking for11// more headroom raises the fee proportionally.12GasPerUgnotFloorint64=10001314// payloadOverheadNum/payloadOverheadDen approximate what a payload really15// costs once the realm has wrapped it in an object. Measured at 1.85x: ten16// 1,024-byte strings in a realm slice cost 18,984 bytes of state, 1,89817// each. It is an estimate and nothing more. The authoritative number is18// the chain's own, from the vm/qstorage query or a StorageDepositEvent.19payloadOverheadNumint64=18520payloadOverheadDenint64=1002122// BytesPerTreeNode is the realm state one balanced-tree container node23// occupies, for the tree containers in this namespace (p/moul/ulist and24// what is built on it).25//26// Measured twice, agreeing exactly. Compacting 8 dead nodes on mainnet27// freed 6,848 bytes (2026-09-23), and 31 nodes in the local integration28// harness freed 26,536. Both give 856.29//30// It is a property of the node, not of the element: the node costs this31// whether the value it carried was 8 bytes or 1,024. That is what makes32// EstimateNodes accurate where EstimateBytes is only indicative.33BytesPerTreeNodeint64=8563435ugnotPerGNOTint64=1_000_00036)
BreakEvenBytes is the fewest bytes whose refund covers a fee: the point where a cleanup stops costing money and starts making it. Below this many bytes the transaction is charity.
EstimateBytes guesses the realm state a payload of this many bytes will occupy, applying the measured object overhead.
It is for sizing a bounty in a Render, where being within a factor of two beats reporting the raw payload length. Never settle an accounting question with it.
EstimateNodes is the realm state a count of dead container nodes occupies, and so what compacting them frees.
Prefer it to EstimateBytes wherever the caller can count, which for a compaction it always can. EstimateBytes scales a payload length by a ratio measured at one size and is wrong at the others: a per-entry floor dominates at the small end, and a realm advertising a bounty that way under-reported by 25x against what the cleanup actually returned on chain (2026-09-23). Counting nodes has no such failure mode. The container reports the exact number, and every node costs the same.
Still an estimate. BytesPerTreeNode is measured rather than derived, and the chain's own StorageDepositEvent remains the only settlement.
FormatGNOT renders ugnot as GNOT with trailing zeros trimmed, because a bounty shown in ugnot is unreadable and gno has no floats.
It delegates to p/moul/kit/num, which owns amount formatting. v0 did it by hand with `amount = -amount`, and since that leaves math.MinInt64 negative both the whole and the fractional part then carried their own sign: FormatGNOT(math.MinInt64) returned "--9223372036854.-775808 GNOT". No caller in this repo can reach it, but the function is exported and a formatter that can return a non-number is not one.
GasFee is the fee a transaction asking for gasWanted must pay at a gas price of num/den ugnot per gas, rounded up.
The fee tracks gas_wanted rather than gas_used, so unused headroom is paid for. Pass the ceiling you will actually put in the transaction, not what you expect to burn.
Refund is the deposit returned for freeing bytes at the given price per byte. Returns 0 for non-positive inputs rather than panicking, so a Render on a realm with no state still works.
Evaluate prices one cleanup: freeing bytes in a transaction asking for gasWanted, at a storage price of pricePerByte and a gas price of num/den ugnot per gas.
1typeQuotestruct{2Bytesint64// bytes the cleanup would free3Refundint64// ugnot returned for them4Feeint64// ugnot the transaction will cost5Netint64// Refund - Fee; negative means it costs more than it pays6BreakEvenint64// bytes needed to cover Fee7}