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

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}