1// Package staking is a simplified, accounting-only port of Synthetix2// StakingRewards to gno.land. Users stake a fungible amount; rewards accrue3// every block, proportional to each staker's share of the total staked,4// at a fixed rewardPerBlock. There is no real coin transfer — balances and5// rewards are plain uint64 accounting (a demonstration of the classic6// "reward-per-token" accumulator pattern).7package staking89import (10 "strconv"1112 "chain"13 "chain/runtime"14 "chain/runtime/unsafe"1516 "gno.land/p/nt/avl/v0"17)1819const (20 // rewardPerBlock is the fixed number of reward units distributed across21 // 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)2627type account struct {28 staked uint64 // currently staked amount29 rewardPaid uint64 // reward-per-token snapshot at the account's last update30 reward uint64 // accrued-but-unclaimed rewards31 minted uint64 // rewards moved into the caller's reward balance32}3334var (35 accounts avl.Tree // address string -> *account36 totalStaked uint6437 rewardPerTokenAcc uint64 // global accumulator, scaled by precision38 lastUpdateBlock uint6439)4041// --- pure helpers (unit-tested) ------------------------------------------4243// computeRewardPerToken advances the accumulator by the rewards earned per44// 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 stored48 }49 return stored + (rate*elapsed*precision)/total50}5152// computeEarned returns total rewards owed to an account given its stake, the53// current accumulator, the accumulator value already paid, and prior accrual.54func computeEarned(balance, rptCurrent, rptPaid, accrued uint64) uint64 {55 return accrued + (balance*(rptCurrent-rptPaid))/precision56}5758// --- internals ------------------------------------------------------------5960func currentBlock() uint64 { return uint64(runtime.ChainHeight()) }6162func 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 acc69}7071// accrue rolls the global accumulator forward to the current block and folds72// the caller's freshly-earned rewards into its stored balance. Mirrors the73// Synthetix `updateReward(account)` modifier.74func accrue(addr string) *account {75 elapsed := currentBlock() - lastUpdateBlock76 rewardPerTokenAcc = computeRewardPerToken(rewardPerTokenAcc, rewardPerBlock, elapsed, totalStaked)77 lastUpdateBlock = currentBlock()7879 acc := getOrCreate(addr)80 acc.reward = computeEarned(acc.staked, rewardPerTokenAcc, acc.rewardPaid, acc.reward)81 acc.rewardPaid = rewardPerTokenAcc82 return acc83}8485// --- exported transactions (crossing) ------------------------------------8687// 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 += amount95 totalStaked += amount96 chain.Emit("Staked", "addr", addr, "amount", strconv.FormatUint(amount, 10))97}9899// 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 -= amount110 totalStaked -= amount111 chain.Emit("Withdrawn", "addr", addr, "amount", strconv.FormatUint(amount, 10))112}113114// GetReward mints all accrued rewards into the caller's reward balance and115// 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.reward120 acc.reward = 0121 acc.minted += claimed122 chain.Emit("RewardPaid", "addr", addr, "amount", strconv.FormatUint(claimed, 10))123 return claimed124}125126// --- read-only ------------------------------------------------------------127128// 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 0134 }135 acc := v.(*account)136 elapsed := currentBlock() - lastUpdateBlock137 rpt := computeRewardPerToken(rewardPerTokenAcc, rewardPerBlock, elapsed, totalStaked)138 return computeEarned(acc.staked, rpt, acc.rewardPaid, acc.reward)139}140141// 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).staked145 }146 return 0147}148149// 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"156157 if accounts.Size() == 0 {158 out += "_No stakers yet. Call `Stake(amount)` to join._\n"159 return out160 }161162 out += "## Stakers\n\n"163 out += "| Address | Staked | Earned (pending) | Minted |\n"164 out += "|---|---|---|---|\n"165166 elapsed := currentBlock() - lastUpdateBlock167 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 false176 })177 return out178}179Earned(addr string) uint64
GetReward(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}) uint64
Render(path string) string
Stake(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, amount uint64)
StakedOf(addr string) uint64
Withdraw(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, amount uint64)
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.