staking.gno
5.91 Kb · 178 lines
1// Package staking is a simplified, accounting-only port of Synthetix
2// StakingRewards to gno.land. Users stake a fungible amount; rewards accrue
3// every block, proportional to each staker's share of the total staked,
4// at a fixed rewardPerBlock. There is no real coin transfer — balances and
5// rewards are plain uint64 accounting (a demonstration of the classic
6// "reward-per-token" accumulator pattern).
7package staking
8
9import (
10 "strconv"
11
12 "chain"
13 "chain/runtime"
14 "chain/runtime/unsafe"
15
16 "gno.land/p/nt/avl/v0"
17)
18
19const (
20 // rewardPerBlock is the fixed number of reward units distributed across
21 // ALL stakers on every block, split by stake share.
22 rewardPerBlock = uint64(100)
23 // precision scales the reward-per-token accumulator to avoid truncation.
24 precision = uint64(1_000_000)
25)
26
27type account struct {
28 staked uint64 // currently staked amount
29 rewardPaid uint64 // reward-per-token snapshot at the account's last update
30 reward uint64 // accrued-but-unclaimed rewards
31 minted uint64 // rewards moved into the caller's reward balance
32}
33
34var (
35 accounts avl.Tree // address string -> *account
36 totalStaked uint64
37 rewardPerTokenAcc uint64 // global accumulator, scaled by precision
38 lastUpdateBlock uint64
39)
40
41// --- pure helpers (unit-tested) ------------------------------------------
42
43// computeRewardPerToken advances the accumulator by the rewards earned per
44// staked unit over `elapsed` blocks. With no stake, the accumulator is frozen.
45func computeRewardPerToken(stored, rate, elapsed, total uint64) uint64 {
46 if total == 0 {
47 return stored
48 }
49 return stored + (rate*elapsed*precision)/total
50}
51
52// computeEarned returns total rewards owed to an account given its stake, the
53// current accumulator, the accumulator value already paid, and prior accrual.
54func computeEarned(balance, rptCurrent, rptPaid, accrued uint64) uint64 {
55 return accrued + (balance*(rptCurrent-rptPaid))/precision
56}
57
58// --- internals ------------------------------------------------------------
59
60func currentBlock() uint64 { return uint64(runtime.ChainHeight()) }
61
62func getOrCreate(addr string) *account {
63 if v := accounts.Get(addr); v != nil {
64 return v.(*account)
65 }
66 acc := &account{}
67 accounts.Set(addr, acc)
68 return acc
69}
70
71// accrue rolls the global accumulator forward to the current block and folds
72// the caller's freshly-earned rewards into its stored balance. Mirrors the
73// Synthetix `updateReward(account)` modifier.
74func accrue(addr string) *account {
75 elapsed := currentBlock() - lastUpdateBlock
76 rewardPerTokenAcc = computeRewardPerToken(rewardPerTokenAcc, rewardPerBlock, elapsed, totalStaked)
77 lastUpdateBlock = currentBlock()
78
79 acc := getOrCreate(addr)
80 acc.reward = computeEarned(acc.staked, rewardPerTokenAcc, acc.rewardPaid, acc.reward)
81 acc.rewardPaid = rewardPerTokenAcc
82 return acc
83}
84
85// --- exported transactions (crossing) ------------------------------------
86
87// Stake adds `amount` to the caller's staked balance.
88func Stake(cur realm, amount uint64) {
89 if amount == 0 {
90 panic("staking: amount must be > 0")
91 }
92 addr := unsafe.PreviousRealm().Address().String()
93 acc := accrue(addr)
94 acc.staked += amount
95 totalStaked += amount
96 chain.Emit("Staked", "addr", addr, "amount", strconv.FormatUint(amount, 10))
97}
98
99// Withdraw removes `amount` from the caller's staked balance.
100func Withdraw(cur realm, amount uint64) {
101 if amount == 0 {
102 panic("staking: amount must be > 0")
103 }
104 addr := unsafe.PreviousRealm().Address().String()
105 acc := accrue(addr)
106 if amount > acc.staked {
107 panic("staking: insufficient staked balance")
108 }
109 acc.staked -= amount
110 totalStaked -= amount
111 chain.Emit("Withdrawn", "addr", addr, "amount", strconv.FormatUint(amount, 10))
112}
113
114// GetReward mints all accrued rewards into the caller's reward balance and
115// resets the accrual counter. Returns the amount minted this call.
116func GetReward(cur realm) uint64 {
117 addr := unsafe.PreviousRealm().Address().String()
118 acc := accrue(addr)
119 claimed := acc.reward
120 acc.reward = 0
121 acc.minted += claimed
122 chain.Emit("RewardPaid", "addr", addr, "amount", strconv.FormatUint(claimed, 10))
123 return claimed
124}
125
126// --- read-only ------------------------------------------------------------
127
128// Earned reports the rewards currently owed to `addr` (accrued but unminted),
129// projected to the current block without mutating any state.
130func Earned(addr address) uint64 {
131 v := accounts.Get(addr.String())
132 if v == nil {
133 return 0
134 }
135 acc := v.(*account)
136 elapsed := currentBlock() - lastUpdateBlock
137 rpt := computeRewardPerToken(rewardPerTokenAcc, rewardPerBlock, elapsed, totalStaked)
138 return computeEarned(acc.staked, rpt, acc.rewardPaid, acc.reward)
139}
140
141// StakedOf reports the current staked balance of `addr`.
142func StakedOf(addr address) uint64 {
143 if v := accounts.Get(addr.String()); v != nil {
144 return v.(*account).staked
145 }
146 return 0
147}
148
149// Render renders pool stats and a stakers table.
150func Render(path string) string {
151 out := "# Staking Rewards\n\n"
152 out += "Simplified Synthetix-style staking (accounting-only, no coin transfer).\n\n"
153 out += "- **Total staked:** " + strconv.FormatUint(totalStaked, 10) + "\n"
154 out += "- **Reward rate:** " + strconv.FormatUint(rewardPerBlock, 10) + " / block (split across stakers)\n"
155 out += "- **Block height:** " + strconv.FormatUint(currentBlock(), 10) + "\n\n"
156
157 if accounts.Size() == 0 {
158 out += "_No stakers yet. Call `Stake(amount)` to join._\n"
159 return out
160 }
161
162 out += "## Stakers\n\n"
163 out += "| Address | Staked | Earned (pending) | Minted |\n"
164 out += "|---|---|---|---|\n"
165
166 elapsed := currentBlock() - lastUpdateBlock
167 rpt := computeRewardPerToken(rewardPerTokenAcc, rewardPerBlock, elapsed, totalStaked)
168 accounts.Iterate("", "", func(key string, value interface{}) bool {
169 acc := value.(*account)
170 earned := computeEarned(acc.staked, rpt, acc.rewardPaid, acc.reward)
171 out += "| " + key +
172 " | " + strconv.FormatUint(acc.staked, 10) +
173 " | " + strconv.FormatUint(earned, 10) +
174 " | " + strconv.FormatUint(acc.minted, 10) + " |\n"
175 return false
176 })
177 return out
178}