> **v0 - Unaudited**
> This is an initial version of this package that has not yet been formally audited.
> A fully audited version will be published as a subsequent release.
> Use in production at your own risk.
# `treasury` - Coin and GRC20 treasury management
Treasury management for coin and GRC20 token transfers in Gno realms. A `Treasury` holds a set of `Banker`s, each responsible for sending a specific asset type, and records the payment history per banker.
# 1. Concepts
- **Treasury**: container that registers one or more `Banker`s and exposes a unified `Send`/`History`/`Balances` API. Also provides a `Render` router for gnoweb pages.
- **Banker**: handler for a single asset type. Built-ins are `CoinsBanker` (native chain coins) and `GRC20Banker` (any number of GRC20 tokens, resolved through a user-supplied `TokenListerFunc`).
- **Payment**: opaque value produced by a banker-specific helper (`NewCoinsPayment`, `NewGRC20Payment`). Each `Payment` is bound to a `BankerID()`, which is how the treasury routes it.
# 2. Usage
```go
import (
"chain"
"chain/banker"
"chain/runtime"
"gno.land/p/nt/grc20/v0"
"gno.land/p/nt/treasury/v0"
)
var (
tokens = map[string]*grc20.Token{}
tr *treasury.Treasury
)
func init() {
owner := runtime.CurrentRealm().Address() // this realm holds and sends the funds
// Coins banker owned by this realm.
coinsBanker, err := treasury.NewCoinsBankerWithOwner(
owner,
banker.NewBanker(banker.BankerTypeRealmSend),
)
if err != nil {
panic(err)
}
// GRC20 banker that resolves tokens through a lister.
grc20Banker, err := treasury.NewGRC20BankerWithOwner(owner, func() map[string]*grc20.Token {
return tokens
})
if err != nil {
panic(err)
}
tr, err = treasury.New(
[]treasury.Banker{coinsBanker, grc20Banker},
runtime.CurrentRealm().PkgPath(),
)
if err != nil {
panic(err)
}
}
// SendUgnot transfers ugnot from the realm to `to`.
func SendUgnot(cur realm, to address, amount int64) {
p := treasury.NewCoinsPayment(chain.Coins{{Denom: "ugnot", Amount: amount}}, to)
if err := tr.Send(0, cur, p); err != nil {
panic(err)
}
}
// Render exposes the treasury under the realm's render path.
func Render(path string) string {
return tr.Render(path)
}
```
# 3. API
## 3.1 Treasury
```go
// Builds a treasury with the provided bankers (at least one required, IDs must be unique).
// pkgPath is the realm's package path, used as the base for the Render router.
func New(bankers []Banker, pkgPath string) (*Treasury, error)
func (t *Treasury) Send(_ int, rlm realm, p Payment) error
func (t *Treasury) History(bankerID string, pageNumber, pageSize int) ([]Payment, error)
func (t *Treasury) Balances(bankerID string) ([]Balance, error)
func (t *Treasury) Address(bankerID string) (string, error)
func (t *Treasury) HasBanker(bankerID string) bool
func (t *Treasury) ListBankerIDs() []string
// Render entry points (a mux router is initialized by `New`).
func (t *Treasury) Render(path string) string
func (t *Treasury) RenderLanding(path string) string
func (t *Treasury) RenderBanker(bankerID, path string) string
func (t *Treasury) RenderBankerHistory(bankerID, path string) string
```
Render routes:
- `""` — landing page, lists each banker.
- `{banker}` — banker details (address, balances, last N payments).
- `{banker}/history` — paginated payment history.
The `history_size` query parameter on `{banker}` controls the preview size (default `5`, `0` hides the preview).
## 3.2 Banker and Payment interfaces
```go
type Banker interface {
ID() string // unique banker ID used for routing
Send(int, realm, Payment) error // thread the caller's cur; pass 0 as the first arg
Balances() []Balance
Address() string // address used to receive payments
}
type Payment interface {
BankerID() string // routes the payment to a banker
String() string
}
type Balance struct {
Denom string
Amount int64
}
// Capability guard: any entry point that accepts a Banker from an external
// caller MUST verify it before invoking its methods. Validates dynamic type
// only (embedding-based wrappers are rejected), not captured state.
func IsCanonicalBanker(b Banker) bool
```
## 3.3 CoinsBanker
`Banker` for native chain coins. Owns an address and an inner `chain/banker.Banker` (must be the canonical one returned by `banker.NewBanker` — fake implementations are rejected).
```go
func NewCoinsBankerWithOwner(owner address, banker_ banker.Banker) (*CoinsBanker, error)
func NewCoinsPayment(coins chain.Coins, toAddress address) Payment
```
`CoinsBanker.ID()` returns `"Coins"`.
## 3.4 GRC20Banker
`Banker` for GRC20 tokens. Tokens are resolved at send time through a `TokenListerFunc`, so the set of supported tokens can change without rebuilding the banker.
```go
type TokenListerFunc func() map[string]*grc20.Token
func NewGRC20BankerWithOwner(owner address, lister TokenListerFunc) (*GRC20Banker, error)
func NewGRC20Payment(tokenKey string, amount int64, toAddress address) Payment
```
`GRC20Banker.ID()` returns `"GRC20"`. `tokenKey` must be a key in the map returned by the lister.
## 3.5 Errors
```go
ErrNoBankerProvided // New called with empty bankers slice
ErrDuplicateBanker // two bankers share the same ID
ErrBankerNotFound // Send/History/... called with an unknown banker ID
ErrSendPaymentFailed // wraps the underlying banker error
ErrCurrentRealmIsNotOwner // banker called from a realm other than its owner
ErrNoOwnerProvided
ErrInvalidPaymentType // payment routed to the wrong banker type
ErrNonCanonicalBanker // CoinsBanker built from a non-canonical std banker
ErrNonCanonicalBankerImpl // New given a Banker of a non-canonical type
ErrSpoofedRealm // Send called with a non-current rlm
ErrNoListerProvided
ErrGRC20TokenNotFound
```
# 4. Security
The `Banker` capability model rests on three rules:
- **Construct your own bankers.** Never accept a pre-built `Banker` (including a `*WithOwner` value) from an external realm. A hostile `Balances`/`Address` can report data tied to an attacker address. `New` calls `IsCanonicalBanker` on each banker and rejects foreign types with `ErrNonCanonicalBankerImpl`.
- **`IsCanonicalBanker` checks dynamic TYPE only, not captured state.** Embedding-based wrappers (`type Evil struct { *CoinsBanker }`) are rejected because type assertions are nominal. Any public entry point that takes a `Banker` from a caller must call it before invoking the banker's methods.
- **Owner must match the acting realm.** `Send` asserts `rlm.IsCurrent()` (else `ErrSpoofedRealm`) and the banker rejects a caller that is not its owner (`ErrCurrentRealmIsNotOwner`). Set the owner to the realm that will actually send.
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.