← Back to Realms

Realm

gno.land/r/gnoswap/staker/v1

Overview

Realm Path
gno.land/r/gnoswap/staker/v1
Exported Functions
14
State Entries
72
Source Files
27
Total Package Entries
144

Exported Functions

14 exported functions

State

72 state entries

Source Code

FILES
assert.gno
go
1package staker
2
3import (
4	"math"
5	"strconv"
6	"strings"
7	"time"
8
9	prbac "gno.land/p/gnoswap/rbac/v1"
10	ufmt "gno.land/p/nt/ufmt/v0"
11
12	"gno.land/r/gnoswap/access/v1"
13)
14
15const (
16	TIMESTAMP_90DAYS  = int64(7776000)
17	TIMESTAMP_180DAYS = int64(15552000)
18	TIMESTAMP_365DAYS = int64(31536000)
19
20	maxIncentiveStartDelay = 7 * 24 * time.Hour
21
22	MAX_UNIX_EPOCH_TIME = 253402300799 // 9999-12-31 23:59:59
23
24	maxUnstakingFee = uint64(1000) // 10%
25)
26
27// assertIsValidAmount ensures the amount is non-negative.
28func assertIsValidAmount(amount int64) {
29	if amount < 0 {
30		panic(makeErrorWithDetails(
31			errInvalidInput,
32			ufmt.Sprintf("amount(%d) must be positive", amount),
33		))
34	}
35}
36
37// assertIsValidRewardAmountFormat ensures the reward amount string is formatted as "tokenPath:amount".
38func assertIsValidRewardAmountFormat(rewardAmountStr string) {
39	parts := strings.SplitN(rewardAmountStr, ":", 2)
40	if len(parts) != 2 {
41		panic(makeErrorWithDetails(
42			errInvalidInput,
43			ufmt.Sprintf("invalid format for SetTokenMinimumRewardAmount params: expected 'tokenPath:amount', got '%s'", rewardAmountStr),
44		))
45	}
46}
47
48// assertIsDepositor ensures the caller is the owner of the deposit.
49func assertIsDepositor(s *stakerV1, caller address, positionId uint64) {
50	deposit := s.getDeposits().get(positionId)
51	if deposit == nil {
52		panic(makeErrorWithDetails(
53			errDataNotFound,
54			ufmt.Sprintf("positionId(%d) not found", positionId),
55		))
56	}
57
58	if caller != deposit.Owner() {
59		panic(makeErrorWithDetails(
60			errNoPermission,
61			ufmt.Sprintf("caller(%s) is not depositor(%s)", caller.String(), deposit.Owner().String()),
62		))
63	}
64}
65
66// assertIsIncentiveOfDeposit ensures the incentive exists and targets the pool the position is staked
67// in, so a caller cannot ask a deposit to collect an incentive that can never pay it.
68// assertIsIncentiveOfPool ensures the incentive targets the pool the position is staked in.
69func assertIsIncentiveOfPool(s *stakerV1, poolPath string, positionId uint64, incentiveId string) {
70	// get panics with errDataNotFound when the incentive does not exist.
71	incentive := s.getExternalIncentives().get(incentiveId)
72
73	if incentive.TargetPoolPath() != poolPath {
74		panic(makeErrorWithDetails(
75			errInvalidInput,
76			ufmt.Sprintf(
77				"incentiveId(%s) targets pool(%s), but positionId(%d) is staked in pool(%s)",
78				incentiveId, incentive.TargetPoolPath(), positionId, poolPath,
79			),
80		))
81	}
82}
83
84// assertHasNoExitCheckpoint ensures the position carries no reward from a previous stake.
85// Collecting it here would smuggle an unbounded calculation into staking, and leaving it would
86// let the next unstake overwrite the checkpoint.
87func assertHasNoExitCheckpoint(s *stakerV1, positionId uint64) {
88	if s.HasUnstakedPosition(positionId) {
89		panic(makeErrorWithDetails(
90			errUncollectedPosition,
91			ufmt.Sprintf("positionId(%d) cannot be staked while its exit checkpoint owes rewards; collect after emission resumes if emission is halted", positionId),
92		))
93	}
94}
95
96// assertHasNoUncollectedIncentive ensures no unstaked position still owes a reward from this
97// incentive, which has not been drawn down from it yet.
98func assertHasNoUncollectedIncentive(s *stakerV1, incentiveId string) {
99	count := s.uncollectedIncentiveCountOf(incentiveId)
100	if count > 0 {
101		panic(makeErrorWithDetails(
102			errUncollectedPosition,
103			ufmt.Sprintf("incentive(%s) is owed to %d unstaked position(s)", incentiveId, count),
104		))
105	}
106}
107
108// assertIsNotStaked ensures the position is not already staked.
109func assertIsNotStaked(s *stakerV1, positionId uint64) {
110	if s.getDeposits().Has(positionId) {
111		panic(makeErrorWithDetails(
112			errAlreadyStaked,
113			ufmt.Sprintf("positionId(%d) already staked", positionId),
114		))
115	}
116}
117
118// assertIsPoolExists ensures the pool exists.
119func assertIsPoolExists(s *stakerV1, poolPath string) {
120	if !s.poolAccessor.ExistsPoolPath(poolPath) {
121		panic(makeErrorWithDetails(
122			errInvalidPoolPath,
123			ufmt.Sprintf("pool(%s) does not exist", poolPath),
124		))
125	}
126}
127
128// assertIsValidPoolTier ensures the tier is within valid range.
129func assertIsValidPoolTier(tier uint64) {
130	if tier >= AllTierCount {
131		panic(makeErrorWithDetails(
132			errInvalidPoolTier,
133			ufmt.Sprintf("tier(%d) must be less than %d", tier, AllTierCount),
134		))
135	}
136}
137
138// assertTier1HasSparePool ensures tier 1 keeps at least one pool after a tier change.
139func assertTier1HasSparePool(currentTier, tier1Count uint64) {
140	if currentTier == Tier1 && tier1Count == 1 {
141		panic(makeErrorWithDetails(errInvalidPoolTier, "tier 1 must have at least one pool"))
142	}
143}
144
145// assertIsGreaterThanMinimumRewardAmount ensures the reward amount meets minimum requirements.
146func assertIsGreaterThanMinimumRewardAmount(s *stakerV1, rewardToken string, rewardAmount int64) {
147	minReward := s.getMinimumRewardAmount()
148
149	if minRewardInt64, found := s.store.GetTokenSpecificMinimumRewards()[rewardToken]; found {
150		minReward = minRewardInt64
151	}
152
153	if rewardAmount < minReward {
154		panic(makeErrorWithDetails(
155			errInvalidInput,
156			ufmt.Sprintf("rewardAmount(%d) is less than minimum required amount(%d)", rewardAmount, minReward),
157		))
158	}
159}
160
161// assertIsAllowedForExternalReward ensures the token is allowed for external rewards.
162func assertIsAllowedForExternalReward(s *stakerV1, poolPath, tokenPath string) {
163	// Operational stop switch: a denied token can never start a NEW incentive,
164	// whether it qualifies as a pool-pair token or through the governance
165	// allowlist. Collection of already-created incentives is deliberately
166	// unaffected; the delivery guard in deliverExternalIncentiveReward bounds
167	// that blast radius instead.
168	if contains(s.store.GetDeniedRewardTokens(), tokenPath) {
169		panic(makeErrorWithDetails(
170			errDeniedRewardToken,
171			ufmt.Sprintf("tokenPath(%s) is denied as an external reward token", tokenPath),
172		))
173	}
174
175	token0, token1, _ := poolPathDivide(poolPath)
176
177	if tokenPath == token0 || tokenPath == token1 {
178		return
179	}
180
181	allowed := contains(s.store.GetAllowedTokens(), tokenPath)
182	if allowed {
183		return
184	}
185
186	panic(makeErrorWithDetails(
187		errNotAllowedForExternalReward,
188		ufmt.Sprintf("tokenPath(%s) is not allowed for external reward for poolPath(%s)", tokenPath, poolPath),
189	))
190}
191
192// assertIsExternalRewardTokenAvailable ensures a pool has no unfinished incentive for the token.
193func assertIsExternalRewardTokenAvailable(s *stakerV1, poolPath, tokenPath string, currentTime int64) {
194	pool := s.getPools().GetPoolOrNil(poolPath)
195	if pool == nil || pool.Incentives() == nil {
196		return
197	}
198
199	minimumActiveIncentiveStartTime := currentTime - TIMESTAMP_365DAYS
200	if minimumActiveIncentiveStartTime < 0 {
201		minimumActiveIncentiveStartTime = 0
202	}
203
204	pool.Incentives().IterateIncentiveIdsByTime(minimumActiveIncentiveStartTime, math.MaxInt64, func(incentiveID string) bool {
205		incentive := s.getExternalIncentives().get(incentiveID)
206		if incentive == nil {
207			return false
208		}
209
210		if incentive.RewardToken() != tokenPath || NewExternalIncentiveResolver(incentive).IsEnded(currentTime) {
211			return false
212		}
213
214		panic(makeErrorWithDetails(
215			errIncentiveAlreadyExists,
216			ufmt.Sprintf(
217				"rewardToken(%s) already has unfinished incentive(%s) for poolPath(%s)",
218				tokenPath,
219				incentiveID,
220				poolPath,
221			),
222		))
223	})
224}
225
226// assertIsValidFeeRate ensures the fee rate is within valid range (0-1000 basis points).
227func assertIsValidFeeRate(fee uint64) {
228	if fee > maxUnstakingFee {
229		panic(makeErrorWithDetails(
230			errInvalidUnstakingFee,
231			ufmt.Sprintf("fee(%d) must be in range 0 ~ %d", fee, maxUnstakingFee),
232		))
233	}
234}
235
236// assertIsValidIncentiveStartTime ensures the incentive starts at midnight no earlier than 24 hours after creation
237// and no later than 7 days after the first eligible midnight.
238func assertIsValidIncentiveStartTime(startTimestamp int64) {
239	// must be in seconds format, not milliseconds
240	// REF: https://stackoverflow.com/a/23982005
241	numStr := strconv.Itoa(int(startTimestamp))
242
243	if len(numStr) >= 13 {
244		panic(makeErrorWithDetails(
245			errInvalidIncentiveStartTime,
246			ufmt.Sprintf("startTimestamp(%d) must be in seconds format, not milliseconds", startTimestamp),
247		))
248	}
249
250	// must be at least 24 hours from now
251	minimumStartTimestamp := getMinimumIncentiveStartTimestamp()
252	if startTimestamp < minimumStartTimestamp {
253		panic(makeErrorWithDetails(
254			errInvalidIncentiveStartTime,
255			ufmt.Sprintf("startTimestamp(%d) must be at least 24 hours later", startTimestamp),
256		))
257	}
258
259	maximumStartTimestamp := getMaximumIncentiveStartTimestamp()
260	if startTimestamp > maximumStartTimestamp {
261		panic(makeErrorWithDetails(
262			errInvalidIncentiveStartTime,
263			ufmt.Sprintf("startTimestamp(%d) exceeds the maximum start time policy", startTimestamp),
264		))
265	}
266
267	// must be midnight of the day
268	startTime := time.Unix(startTimestamp, 0)
269	if !isMidnight(startTime) {
270		panic(makeErrorWithDetails(
271			errInvalidIncentiveStartTime,
272			ufmt.Sprintf("startTime(%d = %s) must be midnight of the day", startTimestamp, startTime.String()),
273		))
274	}
275}
276
277// assertIsAdminGovernanceOrCreator ensures the caller may act on an incentive:
278// admin and governance are the protocol-level operators, and the creator is the
279// address that funded the incentive in CreateExternalIncentive.
280func assertIsAdminGovernanceOrCreator(caller, creator address) {
281	if caller == creator {
282		return
283	}
284	if access.IsAuthorized(prbac.ROLE_ADMIN.String(), caller) ||
285		access.IsAuthorized(prbac.ROLE_GOVERNANCE.String(), caller) {
286		return
287	}
288
289	panic(makeErrorWithDetails(
290		errNoPermission,
291		ufmt.Sprintf(
292			"caller(%s) must be admin, governance, or the incentive creator(%s)",
293			caller.String(), creator.String(),
294		),
295	))
296}
297
298// assertIsNotStartedIncentive ensures the incentive can still be cancelled, i.e.
299// it has not started and nothing has been distributed from it. Only such an
300// incentive may be removed outright; anything that already emitted rewards has
301// to be finalized through EndExternalIncentive so the refund accounting and the
302// penalty balance stay reachable.
303func assertIsNotStartedIncentive(incentiveResolver *ExternalIncentiveResolver, currentTime int64) {
304	if incentiveResolver.IsStarted(currentTime) {
305		panic(makeErrorWithDetails(
306			errCannotCancelIncentive,
307			ufmt.Sprintf(
308				"incentive(%s) already started at %d, current(%d)",
309				incentiveResolver.IncentiveId(), incentiveResolver.StartTimestamp(), currentTime,
310			),
311		))
312	}
313
314	if incentiveResolver.Refunded() {
315		panic(makeErrorWithDetails(
316			errCannotCancelIncentive,
317			ufmt.Sprintf("incentive(%s) has already been refunded", incentiveResolver.IncentiveId()),
318		))
319	}
320
321	// Defensive: a not-yet-started incentive can never have distributed a reward
322	// or accrued a warmup penalty. Removing a record that carries either would
323	// strand those tokens in the staker, so refuse instead of dropping it.
324	if incentiveResolver.DistributedRewardAmount() != 0 || incentiveResolver.AccumulatedPenaltyAmount() != 0 {
325		panic(makeErrorWithDetails(
326			errCannotCancelIncentive,
327			ufmt.Sprintf(
328				"incentive(%s) has already accrued rewards: distributed(%d), penalty(%d)",
329				incentiveResolver.IncentiveId(),
330				incentiveResolver.DistributedRewardAmount(),
331				incentiveResolver.AccumulatedPenaltyAmount(),
332			),
333		))
334	}
335}
336
337// getMinimumIncentiveStartTimestamp returns after 24 hours from now.
338func getMinimumIncentiveStartTimestamp() int64 {
339	return time.Now().Add(24 * time.Hour).Unix()
340}
341
342// getMaximumIncentiveStartTimestamp returns 7 days after the first midnight that satisfies
343// the minimum 24-hour delay.
344func getMaximumIncentiveStartTimestamp() int64 {
345	minimumStartTimestamp := getMinimumIncentiveStartTimestamp()
346	firstEligibleMidnight := time.Unix(minimumStartTimestamp, 0).Truncate(24 * time.Hour)
347	if firstEligibleMidnight.Unix() < minimumStartTimestamp {
348		firstEligibleMidnight = firstEligibleMidnight.Add(24 * time.Hour)
349	}
350
351	return firstEligibleMidnight.Add(maxIncentiveStartDelay).Unix()
352}
353
354// assertIsValidIncentiveEndTime ensures the end timestamp is within valid epoch range.
355func assertIsValidIncentiveEndTime(endTimestamp int64) {
356	if endTimestamp >= MAX_UNIX_EPOCH_TIME {
357		panic(makeErrorWithDetails(
358			errInvalidInput,
359			ufmt.Sprintf("endTimestamp(%d) cannot be later than 253402300799 (9999-12-31 23:59:59)", endTimestamp),
360		))
361	}
362}
363
364// assertIsValidIncentiveDuration ensures the duration is 90, 180, or 365 days.
365func assertIsValidIncentiveDuration(externalDuration int64) {
366	switch externalDuration {
367	case TIMESTAMP_90DAYS, TIMESTAMP_180DAYS, TIMESTAMP_365DAYS:
368		return
369	}
370
371	panic(makeErrorWithDetails(
372		errInvalidIncentiveDuration,
373		ufmt.Sprintf("externalDuration(%d) must be 90, 180, 365 days", externalDuration),
374	))
375}
376
377// AssertIsValidAddress panics if the provided address is invalid.
378func assertIsValidAddress(addr address) {
379	if addr == "" || !addr.IsValid() {
380		panic(makeErrorWithDetails(
381			errInvalidAddress,
382			ufmt.Sprintf("address(%s) is invalid", addr.String()),
383		))
384	}
385}
386
387// isMidnight checks if a time represents midnight (00:00:00).
388func isMidnight(startTime time.Time) bool {
389	hour := startTime.Hour()
390	minute := startTime.Minute()
391	second := startTime.Second()
392
393	return hour == 0 && minute == 0 && second == 0
394}
395
396// assertIsPositionOwner validates that the caller has permission to operate the token.
397func assertIsPositionOwner(owner, caller address) {
398	if owner != caller {
399		panic(makeErrorWithDetails(
400			errNoPermission,
401			ufmt.Sprintf("caller(%s) is not owner of positionId(%s)", caller, owner),
402		))
403	}
404}
405

Raw Package Data

Raw JSON data