Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

vestoken.gno

7.23 Kb · 246 lines
  1// Package vestoken is a fungible token where new supply is never handed out
  2// instantly — it ports the idea behind Solidity's VestingWallet / linear
  3// token-streaming contracts (e.g. Sablier) into a single gno.land realm.
  4//
  5// MintVested opens a linear vesting grant instead of crediting a balance
  6// directly. The beneficiary calls Claim over time to pull whatever has
  7// unlocked so far into their liquid, transferable balance. State is
  8// package-level and persistent; state-mutating functions are crossing
  9// functions (`cur realm`) per the gno 0.9 interrealm convention.
 10package vestoken
 11
 12import (
 13	"chain/runtime"
 14	"errors"
 15	"sort"
 16	"strconv"
 17	"strings"
 18)
 19
 20const (
 21	name   = "VestToken"
 22	symbol = "VEST"
 23)
 24
 25// grant is one linear vesting schedule: `total` unlocks evenly from `start`
 26// to `start+duration` (measured in block height), and `claimed` tracks how
 27// much of the unlocked amount the beneficiary has already pulled.
 28type grant struct {
 29	total    uint64
 30	claimed  uint64
 31	start    int64
 32	duration int64
 33}
 34
 35var (
 36	// balances holds liquid, already-claimed tokens — transferable like a
 37	// normal ERC-20 balance.
 38	balances = map[address]uint64{}
 39	// grants holds at most one active vesting schedule per beneficiary.
 40	grants = map[address]*grant{}
 41	// totalSupply is every token ever minted (vesting or liquid).
 42	totalSupply uint64
 43)
 44
 45var (
 46	errZeroAddress   = errors.New("vestoken: zero address")
 47	errZeroAmount    = errors.New("vestoken: amount must be positive")
 48	errZeroDuration  = errors.New("vestoken: duration must be positive")
 49	errGrantActive   = errors.New("vestoken: beneficiary already has an unclaimed grant")
 50	errNoGrant       = errors.New("vestoken: no active grant for this account")
 51	errInsufficient  = errors.New("vestoken: insufficient balance")
 52	errOverflow      = errors.New("vestoken: supply overflow")
 53	errSpoofedRealm  = errors.New("vestoken: spoofed realm")
 54	errNotUser       = errors.New("vestoken: caller is not a user account")
 55)
 56
 57// caller returns the immediate caller's address for a crossing function,
 58// enforcing the gno 0.9 authentication primitive: cur must be the live
 59// crossing frame before its Previous() is trusted.
 60func caller(cur realm) address {
 61	if !cur.IsCurrent() {
 62		panic(errSpoofedRealm)
 63	}
 64	prev := cur.Previous()
 65	if !prev.IsUser() {
 66		panic(errNotUser)
 67	}
 68	return prev.Address()
 69}
 70
 71// MintVested opens a new linear vesting grant for `to`: `amount` tokens
 72// unlock block-by-block over the next `durationBlocks` blocks, starting now.
 73// A beneficiary may only hold one active grant at a time. Crossing function.
 74func MintVested(cur realm, to address, amount uint64, durationBlocks int64) {
 75	_ = caller(cur) // authenticate the crossing frame; anyone may open a grant in this demo
 76	if !to.IsValid() {
 77		panic(errZeroAddress)
 78	}
 79	if amount == 0 {
 80		panic(errZeroAmount)
 81	}
 82	if durationBlocks <= 0 {
 83		panic(errZeroDuration)
 84	}
 85	if g, ok := grants[to]; ok && g.claimed < g.total {
 86		panic(errGrantActive)
 87	}
 88	if totalSupply+amount < totalSupply {
 89		panic(errOverflow)
 90	}
 91	totalSupply += amount
 92	grants[to] = &grant{
 93		total:    amount,
 94		start:    runtime.ChainHeight(),
 95		duration: durationBlocks,
 96	}
 97}
 98
 99// Claim pulls whatever has unlocked so far from the caller's grant into
100// their liquid balance, and returns how much was claimed. Crossing function.
101func Claim(cur realm) uint64 {
102	who := caller(cur)
103	g, ok := grants[who]
104	if !ok {
105		panic(errNoGrant)
106	}
107
108	unlocked := vestedAmount(g, runtime.ChainHeight())
109	claimable := unlocked - g.claimed
110	if claimable == 0 {
111		return 0
112	}
113
114	g.claimed = unlocked
115	balances[who] += claimable
116	if g.claimed >= g.total {
117		delete(grants, who)
118	}
119	return claimable
120}
121
122// vestedAmount computes how much of g has unlocked by the given height.
123func vestedAmount(g *grant, height int64) uint64 {
124	elapsed := height - g.start
125	if elapsed <= 0 {
126		return 0
127	}
128	if elapsed >= g.duration {
129		return g.total
130	}
131	return g.total * uint64(elapsed) / uint64(g.duration)
132}
133
134// Transfer moves `amount` of the caller's liquid (already-claimed) balance
135// to `to`. Crossing function.
136func Transfer(cur realm, to address, amount uint64) {
137	from := caller(cur)
138	if !to.IsValid() {
139		panic(errZeroAddress)
140	}
141	if amount == 0 {
142		return
143	}
144	if balances[from] < amount {
145		panic(errInsufficient)
146	}
147	balances[from] -= amount
148	balances[to] += amount
149}
150
151// BalanceOf returns the liquid (claimed, transferable) balance of holder.
152func BalanceOf(holder address) uint64 {
153	return balances[holder]
154}
155
156// GrantOf returns the beneficiary's active grant, if any: total amount,
157// amount claimed so far, and the amount currently claimable.
158func GrantOf(beneficiary address) (total uint64, claimed uint64, claimable uint64) {
159	g, ok := grants[beneficiary]
160	if !ok {
161		return 0, 0, 0
162	}
163	unlocked := vestedAmount(g, runtime.ChainHeight())
164	return g.total, g.claimed, unlocked - g.claimed
165}
166
167// TotalSupply returns every token ever minted, vesting or liquid.
168func TotalSupply() uint64 { return totalSupply }
169
170// Name returns the token name.
171func Name() string { return name }
172
173// Symbol returns the token symbol.
174func Symbol() string { return symbol }
175
176// Render produces the gnoweb Markdown view: metadata, active vesting
177// grants, and liquid balances — both tables sorted by address for
178// deterministic output. Render is NOT a crossing function.
179func Render(path string) string {
180	var b strings.Builder
181
182	b.WriteString("# " + name + " (" + symbol + ")\n\n")
183	b.WriteString("A linearly-vesting ERC-20-style token on gno.land — ")
184	b.WriteString("new supply unlocks block-by-block instead of landing instantly.\n\n")
185	b.WriteString("- **Name:** " + name + "\n")
186	b.WriteString("- **Symbol:** " + symbol + "\n")
187	b.WriteString("- **Total supply:** " + strconv.FormatUint(totalSupply, 10) + "\n")
188	b.WriteString("- **Current height:** " + strconv.FormatInt(runtime.ChainHeight(), 10) + "\n\n")
189
190	b.WriteString("## Active grants\n\n")
191	grantKeys := sortedAddrKeys(grantAddrs())
192	if len(grantKeys) == 0 {
193		b.WriteString("_No active grants — call `MintVested` to open one._\n\n")
194	} else {
195		b.WriteString("| Beneficiary | Total | Claimed | Claimable now |\n")
196		b.WriteString("| --- | ---: | ---: | ---: |\n")
197		for _, k := range grantKeys {
198			g := grants[address(k)]
199			unlocked := vestedAmount(g, runtime.ChainHeight())
200			b.WriteString("| `" + k + "` | " +
201				strconv.FormatUint(g.total, 10) + " | " +
202				strconv.FormatUint(g.claimed, 10) + " | " +
203				strconv.FormatUint(unlocked-g.claimed, 10) + " |\n")
204		}
205		b.WriteString("\n")
206	}
207
208	b.WriteString("## Liquid balances\n\n")
209	balKeys := sortedAddrKeys(balanceAddrs())
210	if len(balKeys) == 0 {
211		b.WriteString("_No claimed balances yet — call `Claim` once a grant has unlocked._\n")
212		return b.String()
213	}
214	b.WriteString("| Address | Amount |\n")
215	b.WriteString("| --- | ---: |\n")
216	for _, k := range balKeys {
217		b.WriteString("| `" + k + "` | " +
218			strconv.FormatUint(balances[address(k)], 10) + " |\n")
219	}
220
221	return b.String()
222}
223
224func grantAddrs() []string {
225	keys := make([]string, 0, len(grants))
226	for addr := range grants {
227		keys = append(keys, addr.String())
228	}
229	return keys
230}
231
232func balanceAddrs() []string {
233	keys := make([]string, 0, len(balances))
234	for addr, bal := range balances {
235		if bal == 0 {
236			continue
237		}
238		keys = append(keys, addr.String())
239	}
240	return keys
241}
242
243func sortedAddrKeys(keys []string) []string {
244	sort.Strings(keys)
245	return keys
246}