Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}