Search Apps Documentation Source Content File Folder Download Copy Actions Download 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}