Search Apps Documentation Source Content File Folder Download Copy Actions Download

external_incentive.gno

12.24 Kb ยท 376 lines
  1package v1
  2
  3import (
  4	"chain"
  5	"chain/runtime"
  6	"math"
  7	"time"
  8
  9	prbac "gno.land/p/gnoswap/rbac"
 10	"gno.land/p/nt/avl"
 11	"gno.land/p/nt/ufmt"
 12
 13	"gno.land/r/gnoswap/access"
 14	"gno.land/r/gnoswap/common"
 15	en "gno.land/r/gnoswap/emission"
 16	"gno.land/r/gnoswap/gns"
 17	"gno.land/r/gnoswap/halt"
 18	sr "gno.land/r/gnoswap/staker"
 19)
 20
 21// CreateExternalIncentive creates an external incentive program for a pool.
 22//
 23// Parameters:
 24//   - targetPoolPath: pool to incentivize
 25//   - rewardToken: reward token path
 26//   - rewardAmount: total reward amount
 27//   - startTimestamp, endTimestamp: incentive period
 28//
 29// Only callable by users.
 30func (s *stakerV1) CreateExternalIncentive(
 31	targetPoolPath string,
 32	rewardToken string, // token path should be registered
 33	rewardAmount int64,
 34	startTimestamp int64,
 35	endTimestamp int64,
 36) {
 37	halt.AssertIsNotHaltedStaker()
 38
 39	prevRealm := runtime.PreviousRealm()
 40	caller := prevRealm.Address()
 41	access.AssertIsUser(prevRealm)
 42
 43	assertIsPoolExists(s, targetPoolPath)
 44	assertIsGreaterThanMinimumRewardAmount(s, rewardToken, rewardAmount)
 45	assertIsAllowedForExternalReward(s, targetPoolPath, rewardToken)
 46	assertIsValidIncentiveStartTime(startTimestamp)
 47	assertIsValidIncentiveEndTime(endTimestamp)
 48	assertIsValidIncentiveDuration(safeSubInt64(endTimestamp, startTimestamp))
 49	// assert that the user has sent the correct amount of native coin
 50	assertIsValidUserCoinSend(rewardToken, rewardAmount)
 51
 52	en.MintAndDistributeGns(cross)
 53
 54	stakerAddr := access.MustGetAddress(prbac.ROLE_STAKER.String())
 55
 56	// transfer reward token from user to staker
 57	isRequestUnwrap := false
 58	if rewardToken == GNOT_DENOM {
 59		isRequestUnwrap = true
 60		rewardToken = WUGNOT_PATH
 61		err := wrapWithTransfer(stakerAddr, rewardAmount)
 62		if err != nil {
 63			panic(err)
 64		}
 65	} else {
 66		common.SafeGRC20TransferFrom(cross, rewardToken, caller, stakerAddr, rewardAmount)
 67	}
 68
 69	depositGnsAmount := s.store.GetDepositGnsAmount()
 70
 71	// deposit gns amount
 72	gns.TransferFrom(cross, caller, stakerAddr, depositGnsAmount)
 73
 74	currentTime := time.Now().Unix()
 75	currentHeight := runtime.ChainHeight()
 76	incentiveId := s.store.NextIncentiveID(caller, currentTime)
 77	pool := s.getPools().GetPoolOrNil(targetPoolPath)
 78	if pool == nil {
 79		pool = sr.NewPool(targetPoolPath, currentTime)
 80		s.getPools().set(targetPoolPath, pool)
 81	}
 82
 83	incentive := sr.NewExternalIncentive(
 84		incentiveId,
 85		targetPoolPath,
 86		rewardToken,
 87		rewardAmount,
 88		startTimestamp,
 89		endTimestamp,
 90		caller,
 91		depositGnsAmount,
 92		currentHeight,
 93		currentTime,
 94		isRequestUnwrap,
 95	)
 96
 97	externalIncentives := s.store.GetExternalIncentives()
 98	if externalIncentives.Has(incentiveId) {
 99		panic(makeErrorWithDetails(
100			errIncentiveAlreadyExists,
101			ufmt.Sprintf("incentiveId(%s)", incentiveId),
102		))
103	}
104	// store external incentive information for each incentiveId
105	externalIncentives.Set(incentiveId, incentive)
106
107	poolResolver := NewPoolResolver(pool)
108	poolResolver.IncentivesResolver().create(caller, incentive)
109
110	// add incentive to time-based index for lazy discovery during CollectReward
111	s.addIncentiveIdByCreationTime(targetPoolPath, incentiveId, currentTime)
112
113	chain.Emit(
114		"CreateExternalIncentive",
115		"prevAddr", caller.String(),
116		"prevRealm", prevRealm.PkgPath(),
117		"incentiveId", incentiveId,
118		"targetPoolPath", targetPoolPath,
119		"rewardToken", rewardToken,
120		"rewardAmount", formatAnyInt(rewardAmount),
121		"startTimestamp", formatAnyInt(startTimestamp),
122		"endTimestamp", formatAnyInt(endTimestamp),
123		"depositGnsAmount", formatAnyInt(depositGnsAmount),
124		"currentHeight", formatAnyInt(currentHeight),
125		"currentTime", formatAnyInt(currentTime),
126	)
127}
128
129// EndExternalIncentive ends an external incentive and refunds remaining rewards.
130//
131// Finalizes incentive program after end timestamp.
132// Returns unallocated rewards and GNS deposit.
133// Calculates unclaimable rewards for refund.
134//
135// Parameters:
136//   - targetPoolPath: Pool with the incentive
137//   - incentiveId: Unique incentive identifier
138//
139// Process:
140//  1. Validates incentive end time reached
141//  2. Calculates remaining and unclaimable rewards
142//  3. Refunds rewards to original creator
143//  4. Returns 100 GNS deposit
144//  5. Removes incentive from active list
145//
146// Only callable by Refundee or Admin.
147func (s *stakerV1) EndExternalIncentive(targetPoolPath, incentiveId string) {
148	halt.AssertIsNotHaltedStaker()
149	halt.AssertIsNotHaltedWithdraw()
150
151	// checks pool registry
152	assertIsPoolExists(s, targetPoolPath)
153
154	// checks if the pool has been incentivized
155	pool, exists := s.getPools().Get(targetPoolPath)
156	if !exists {
157		panic(makeErrorWithDetails(
158			errDataNotFound,
159			ufmt.Sprintf("targetPoolPath(%s) not found", targetPoolPath),
160		))
161	}
162
163	poolResolver := NewPoolResolver(pool)
164	incentivesResolver := poolResolver.IncentivesResolver()
165
166	// Get incentive to check if GNS already refunded
167	incentiveResolver, exists := incentivesResolver.GetIncentiveResolver(incentiveId)
168	if !exists {
169		panic(makeErrorWithDetails(
170			errCannotEndIncentive,
171			ufmt.Sprintf("cannot end non existent incentive(%s)", incentiveId),
172		))
173	}
174
175	// Check if incentive has already been refunded
176	if incentiveResolver.Refunded() {
177		panic(makeErrorWithDetails(
178			errCannotEndIncentive,
179			ufmt.Sprintf("incentive(%s) has already been refunded", incentiveId),
180		))
181	}
182
183	caller := runtime.PreviousRealm().Address()
184
185	// Process ending
186	incentive, refund, err := s.endExternalIncentive(poolResolver, incentiveResolver, caller, time.Now().Unix())
187	if err != nil {
188		panic(err)
189	}
190
191	// remove incentive from time-based index
192	s.removeIncentiveIdByCreationTime(targetPoolPath, incentiveId, incentive.CreatedTimestamp())
193
194	stakerAddr := access.MustGetAddress(prbac.ROLE_STAKER.String())
195	poolLeftExternalRewardAmount := common.BalanceOf(incentiveResolver.RewardToken(), stakerAddr)
196	if poolLeftExternalRewardAmount < refund {
197		refund = poolLeftExternalRewardAmount
198	}
199
200	// handle refund based on original token type
201	if incentiveResolver.IsRequestUnwrap() {
202		// unwrap to GNOT if originally deposited as native GNOT
203		transferErr := unwrapWithTransfer(incentiveResolver.Refundee(), refund)
204		if transferErr != nil {
205			panic(transferErr)
206		}
207	} else {
208		// keep as WUGNOT or other token if originally deposited as wrapped token
209		common.SafeGRC20Transfer(cross, incentiveResolver.RewardToken(), incentiveResolver.Refundee(), refund)
210	}
211
212	// Transfer GNS deposit back to refundee
213	gns.Transfer(cross, incentiveResolver.Refundee(), incentiveResolver.DepositGnsAmount())
214
215	// Mark incentive as refunded and update
216	// After this update, attempts to re-claim GNS or rewards that were deposited
217	// through the `endExternalIncentive` function will be blocked.
218	incentiveResolver.SetRefunded(true)
219	incentiveResolver.addDistributedRewardAmount(refund)
220	incentivesResolver.update(incentive.Refundee(), incentive)
221
222	previousRealm := runtime.PreviousRealm()
223	chain.Emit(
224		"EndExternalIncentive",
225		"prevAddr", previousRealm.Address().String(),
226		"prevRealm", previousRealm.PkgPath(),
227		"incentiveId", incentiveId,
228		"targetPoolPath", targetPoolPath,
229		"refundee", incentiveResolver.Refundee().String(),
230		"refundToken", incentiveResolver.RewardToken(),
231		"refundAmount", formatAnyInt(refund),
232		"refundGnsAmount", formatAnyInt(incentiveResolver.DepositGnsAmount()),
233		"isRequestUnwrap", formatBool(incentiveResolver.IsRequestUnwrap()),
234		"externalIncentiveEndBy", previousRealm.Address().String(),
235	)
236}
237
238// endExternalIncentive processes the end of an external incentive program.
239func (s *stakerV1) endExternalIncentive(resolver *PoolResolver, incentiveResolver *ExternalIncentiveResolver, caller address, currentTime int64) (*sr.ExternalIncentive, int64, error) {
240	if currentTime < incentiveResolver.EndTimestamp() {
241		return nil, 0, makeErrorWithDetails(
242			errCannotEndIncentive,
243			ufmt.Sprintf("cannot end incentive before endTime(%d), current(%d)", incentiveResolver.EndTimestamp(), currentTime),
244		)
245	}
246
247	// only refundee or admin can end incentive
248	if !access.IsAuthorized(prbac.ROLE_ADMIN.String(), caller) && caller != incentiveResolver.Refundee() {
249		adminAddr := access.MustGetAddress(prbac.ROLE_ADMIN.String())
250		return nil, 0, makeErrorWithDetails(
251			errNoPermission,
252			ufmt.Sprintf(
253				"only refundee(%s) or admin(%s) can end incentive, but called from %s",
254				incentiveResolver.Refundee(), adminAddr.String(), caller,
255			),
256		)
257	}
258
259	totalReward := int64(0)
260
261	// calculate total external reward for the incentive
262	s.getDeposits().IterateByPoolPath(0, math.MaxUint64, incentiveResolver.TargetPoolPath(), func(positionId uint64, deposit *sr.Deposit) bool {
263		depositResolver := NewDepositResolver(deposit)
264		lastCollectTime := depositResolver.ExternalRewardLastCollectTime(incentiveResolver.IncentiveId())
265
266		if lastCollectTime > incentiveResolver.EndTimestamp() {
267			return false
268		}
269
270		rewardState := resolver.RewardStateOf(deposit)
271		calculatedTotalReward := rewardState.calculateCollectableExternalReward(lastCollectTime, currentTime, incentiveResolver.ExternalIncentive)
272		totalReward = safeAddInt64(totalReward, calculatedTotalReward)
273
274		return false
275	})
276
277	// calculate refund amount is the difference between the incentive reward amount and the total external reward
278	refund := safeSubInt64(incentiveResolver.TotalRewardAmount(), totalReward)
279	refund = safeSubInt64(refund, incentiveResolver.DistributedRewardAmount())
280
281	return incentiveResolver.ExternalIncentive, refund, nil
282}
283
284// addIncentiveIdByCreationTime adds an external incentive to the time-based index.
285//
286// The index structure is:
287//   - creationTime (int64) -> poolPath (string) -> []incentiveIds
288func (s *stakerV1) addIncentiveIdByCreationTime(poolPath, incentiveId string, creationTime int64) {
289	incentivesByTime := s.getExternalIncentivesByCreationTime()
290
291	currentPoolIncentiveIdsValue, exists := incentivesByTime.Get(creationTime)
292	if !exists {
293		currentPoolIncentiveIdsValue = avl.NewTree()
294	}
295
296	currentPoolIncentiveIds, ok := currentPoolIncentiveIdsValue.(*avl.Tree)
297	if !ok {
298		panic(ufmt.Sprintf("invalid type in incentivesByTime tree: expected *avl.Tree, got %T", currentPoolIncentiveIdsValue))
299	}
300
301	incentiveIdsValue, exists := currentPoolIncentiveIds.Get(poolPath)
302	if !exists {
303		incentiveIdsValue = []string{}
304	}
305
306	incentiveIds, ok := incentiveIdsValue.([]string)
307	if !ok {
308		panic(ufmt.Sprintf("invalid type in incentivesByTime tree: expected []string, got %T", incentiveIdsValue))
309	}
310
311	incentiveIds = append(incentiveIds, incentiveId)
312	currentPoolIncentiveIds.Set(poolPath, incentiveIds)
313	incentivesByTime.Set(creationTime, currentPoolIncentiveIds)
314
315	s.updateExternalIncentivesByCreationTime(incentivesByTime)
316}
317
318// removeIncentiveIdByCreationTime removes an incentive from the time-based index.
319// This is called when an external incentive is ended via EndExternalIncentive.
320//
321// The index structure is:
322//   - creationTime (int64) -> poolPath (string) -> []incentiveId
323func (s *stakerV1) removeIncentiveIdByCreationTime(poolPath, incentiveId string, creationTime int64) {
324	incentivesByTime := s.getExternalIncentivesByCreationTime()
325
326	// check if creation time exists
327	currentPoolIncentiveIdsValue, exists := incentivesByTime.Get(creationTime)
328	if !exists {
329		return
330	}
331
332	currentPoolIncentiveIds, ok := currentPoolIncentiveIdsValue.(*avl.Tree)
333	if !ok {
334		panic(ufmt.Sprintf("invalid type in incentivesByTime tree: expected *avl.Tree, got %T", currentPoolIncentiveIdsValue))
335	}
336
337	// check if pool path exists at this time
338	incentiveIdsValue, exists := currentPoolIncentiveIds.Get(poolPath)
339	if !exists {
340		return
341	}
342
343	incentiveIds, ok := incentiveIdsValue.([]string)
344	if !ok {
345		panic(ufmt.Sprintf("invalid type in incentivesByTime tree: expected []string, got %T", incentiveIdsValue))
346	}
347
348	// if only incentive exists for this pool,remove the entire pool entry
349	if len(incentiveIds) == 1 && incentiveIds[0] == incentiveId {
350		currentPoolIncentiveIds.Remove(poolPath)
351
352		// if no pools remain at this time, remove the time entry
353		if currentPoolIncentiveIds.Size() == 0 {
354			incentivesByTime.Remove(creationTime)
355		} else {
356			incentivesByTime.Set(creationTime, currentPoolIncentiveIds)
357		}
358
359		s.updateExternalIncentivesByCreationTime(incentivesByTime)
360
361		return
362	}
363
364	// remove only the target incentive from slice
365	for index, id := range incentiveIds {
366		if id == incentiveId {
367			incentiveIds = append(incentiveIds[:index], incentiveIds[index+1:]...)
368			break
369		}
370	}
371
372	currentPoolIncentiveIds.Set(poolPath, incentiveIds)
373	incentivesByTime.Set(creationTime, currentPoolIncentiveIds)
374
375	s.updateExternalIncentivesByCreationTime(incentivesByTime)
376}