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

launchpad_project.gno

17.02 Kb · 568 lines
  1package launchpad
  2
  3import (
  4	"chain"
  5	"chain/runtime"
  6	"errors"
  7	"strconv"
  8	"strings"
  9	"time"
 10
 11	gnsmath "gno.land/p/gnoswap/gnsmath"
 12	"gno.land/p/gnoswap/utils"
 13	ufmt "gno.land/p/nt/ufmt/v0"
 14
 15	"gno.land/r/gnoswap/access"
 16	"gno.land/r/gnoswap/common"
 17	"gno.land/r/gnoswap/emission"
 18	"gno.land/r/gnoswap/halt"
 19	"gno.land/r/gnoswap/launchpad"
 20)
 21
 22// CreateProject creates a new launchpad project with tiered allocations.
 23//
 24// Parameters:
 25//   - name: project name
 26//   - tokenPath: reward token contract path
 27//   - recipient: project recipient address
 28//   - depositAmount: amount of tokens to deposit
 29//   - conditionTokens: comma-separated token paths for conditions
 30//   - conditionAmounts: comma-separated minimum amounts for conditions
 31//   - tier30Ratio: allocation ratio for 30-day tier
 32//   - tier90Ratio: allocation ratio for 90-day tier
 33//   - tier180Ratio: allocation ratio for 180-day tier
 34//   - startTime: unix timestamp for project start
 35//
 36// Returns project ID.
 37// Only callable by admin or governance.
 38func (lp *launchpadV1) CreateProject(
 39	_ int,
 40	rlm realm,
 41	name string,
 42	tokenPath string,
 43	recipient address,
 44	depositAmount int64,
 45	conditionTokens string,
 46	conditionAmounts string,
 47	tier30Ratio int64,
 48	tier90Ratio int64,
 49	tier180Ratio int64,
 50	startTime int64,
 51) string {
 52	if !rlm.IsCurrent() {
 53		panic(errors.New(errSpoofedRealm))
 54	}
 55
 56	halt.AssertIsNotHaltedLaunchpad()
 57
 58	previousRealm := rlm.Previous()
 59	caller := previousRealm.Address()
 60	access.AssertIsAdminOrGovernance(caller)
 61
 62	launchpadAddr := rlm.Address()
 63	currentHeight := runtime.ChainHeight()
 64	currentTime := time.Now().Unix()
 65
 66	params := &createProjectParams{
 67		name:                  name,
 68		tokenPath:             tokenPath,
 69		recipient:             recipient,
 70		depositAmount:         depositAmount,
 71		conditionTokens:       conditionTokens,
 72		conditionAmounts:      conditionAmounts,
 73		tier30Ratio:           tier30Ratio,
 74		tier90Ratio:           tier90Ratio,
 75		tier180Ratio:          tier180Ratio,
 76		startTime:             startTime,
 77		currentTime:           currentTime,
 78		currentHeight:         currentHeight,
 79		minimumStartDelayTime: projectMinimumStartDelayTime,
 80	}
 81
 82	// Checks: validate balance before creating project
 83	tokenBalance := common.BalanceOf(tokenPath, caller)
 84	if tokenBalance < depositAmount {
 85		panic(
 86			makeErrorWithDetails(
 87				errInsufficientBalance, ufmt.Sprintf(
 88					"caller(%s) balance(%d) < depositAmount(%d)",
 89					caller.String(), tokenBalance, depositAmount,
 90				),
 91			),
 92		)
 93	}
 94
 95	// Effects: create project and save state
 96	project, err := lp.createProject(0, rlm, params)
 97	if err != nil {
 98		panic(err)
 99	}
100
101	// Interactions: transfer tokens
102	common.SafeGRC20TransferFrom(
103		cross(rlm),
104		tokenPath,
105		caller,
106		launchpadAddr,
107		depositAmount,
108	)
109
110	tier30, err := getProjectTier(project, projectTier30)
111	if err != nil {
112		panic(err)
113	}
114
115	tier90, err := getProjectTier(project, projectTier90)
116	if err != nil {
117		panic(err)
118	}
119
120	tier180, err := getProjectTier(project, projectTier180)
121	if err != nil {
122		panic(err)
123	}
124
125	conditionEventAttrs := buildConditionEventAttrs(params.conditionTokens, params.conditionAmounts)
126
127	eventAttrs := append([]string{
128		"prevAddr", caller.String(),
129		"prevRealm", previousRealm.PkgPath(),
130		"name", name,
131		"tokenPath", tokenPath,
132		"recipient", recipient.String(),
133		"depositAmount", utils.FormatInt(depositAmount),
134		"tier30Ratio", utils.FormatInt(params.tier30Ratio),
135		"tier90Ratio", utils.FormatInt(params.tier90Ratio),
136		"tier180Ratio", utils.FormatInt(params.tier180Ratio),
137		"startTime", utils.FormatInt(params.startTime),
138		"projectId", project.ID(),
139		"tier30Amount", utils.FormatInt(tier30.TotalDistributeAmount()),
140		"tier30EndTime", utils.FormatInt(tier30.EndTime()),
141		"tier90Amount", utils.FormatInt(tier90.TotalDistributeAmount()),
142		"tier90EndTime", utils.FormatInt(tier90.EndTime()),
143		"tier180Amount", utils.FormatInt(tier180.TotalDistributeAmount()),
144		"tier180EndTime", utils.FormatInt(tier180.EndTime()),
145	}, conditionEventAttrs...)
146
147	chain.Emit(
148		"CreateProject",
149		eventAttrs...,
150	)
151
152	return project.ID()
153}
154
155// createProject creates a new project with the given parameters.
156// This function validates the input parameters, creates the project structure,
157// and sets up the project tiers and reward managers.
158// Returns the created project and any error.
159func (lp *launchpadV1) createProject(_ int, rlm realm, params *createProjectParams) (*launchpad.Project, error) {
160	if err := params.validate(); err != nil {
161		return nil, err
162	}
163
164	// create project
165	project := launchpad.NewProject(
166		params.name,
167		params.tokenPath,
168		params.depositAmount,
169		params.recipient,
170		params.currentHeight,
171		params.currentTime,
172	)
173
174	// Get state components
175	projects := lp.store.GetProjects()
176	projectTierRewardManagers := lp.store.GetProjectTierRewardManagers()
177
178	// check duplicate project
179	if projects.Has(project.ID()) {
180		return nil, makeErrorWithDetails(
181			errDuplicateProject,
182			ufmt.Sprintf("project(%s) already exists", project.ID()),
183		)
184	}
185
186	projectConditions, err := launchpad.NewProjectConditionsWithError(params.conditionTokens, params.conditionAmounts)
187	if err != nil {
188		return nil, err
189	}
190
191	for _, condition := range projectConditions {
192		addProjectCondition(project, condition.TokenPath(), condition)
193	}
194
195	projectTierRatios := map[int64]int64{
196		projectTier30:  params.tier30Ratio,
197		projectTier90:  params.tier90Ratio,
198		projectTier180: params.tier180Ratio,
199	}
200
201	accumulatedTierDistributeAmount := int64(0)
202
203	for _, duration := range projectTierDurations {
204		rewardCollectableDuration := projectTierRewardCollectableDuration[duration]
205		tierDurationTime := projectTierDurationTimes[duration]
206		tierDistributeAmount := gnsmath.SafeMulDivInt64(params.depositAmount, projectTierRatios[duration], 100)
207		accumulatedTierDistributeAmount = gnsmath.SafeAddInt64(accumulatedTierDistributeAmount, tierDistributeAmount)
208
209		// if the last tier, distribute the remaining amount
210		if duration == projectTier180 {
211			remainTierDistributeAmount := gnsmath.SafeSubInt64(params.depositAmount, accumulatedTierDistributeAmount)
212			tierDistributeAmount = gnsmath.SafeAddInt64(tierDistributeAmount, remainTierDistributeAmount)
213		}
214
215		projectTier := newProjectTier(
216			project.ID(),
217			duration,
218			tierDistributeAmount,
219			params.startTime,
220			params.startTime+tierDurationTime,
221		)
222		addProjectTier(project, duration, projectTier)
223
224		projectTierRewardManagers.Set(projectTier.ID(), newRewardManager(
225			projectTier.TotalDistributeAmount(),
226			projectTier.StartTime(),
227			projectTier.EndTime(),
228			rewardCollectableDuration,
229		))
230	}
231
232	project.SetTiersRatios(projectTierRatios)
233	projects.Set(project.ID(), project)
234
235	// Save the modified state back
236	if err := lp.store.SetProjects(0, rlm, projects); err != nil {
237		return nil, err
238	}
239	if err := lp.store.SetProjectTierRewardManagers(0, rlm, projectTierRewardManagers); err != nil {
240		return nil, err
241	}
242
243	return project, nil
244}
245
246// TransferLeftFromProjectByAdmin transfers the remaining rewards of a project to a specified recipient.
247// Only admin can call this function. Returns the amount of rewards transferred.
248func (lp *launchpadV1) TransferLeftFromProjectByAdmin(_ int, rlm realm, projectID string, recipient address) int64 {
249	if !rlm.IsCurrent() {
250		panic(errors.New(errSpoofedRealm))
251	}
252
253	halt.AssertIsNotHaltedLaunchpad()
254
255	previousRealm := rlm.Previous()
256	caller := previousRealm.Address()
257	access.AssertIsAdmin(caller)
258
259	currentHeight := runtime.ChainHeight()
260	currentTime := time.Now().Unix()
261
262	project, err := lp.getProject(projectID)
263	if err != nil {
264		panic(err)
265	}
266
267	projectLeftReward, err := lp.transferLeftFromProject(0, rlm, project, recipient, currentTime)
268	if err != nil {
269		panic(err)
270	}
271
272	tier30, err := getProjectTier(project, projectTier30)
273	if err != nil {
274		panic(err)
275	}
276
277	tier90, err := getProjectTier(project, projectTier90)
278	if err != nil {
279		panic(err)
280	}
281
282	tier180, err := getProjectTier(project, projectTier180)
283	if err != nil {
284		panic(err)
285	}
286
287	chain.Emit(
288		"TransferLeftFromProjectByAdmin",
289		"prevAddr", caller.String(),
290		"prevRealm", previousRealm.PkgPath(),
291		"projectId", projectID,
292		"recipient", recipient.String(),
293		"tokenPath", project.TokenPath(),
294		"leftReward", utils.FormatInt(projectLeftReward),
295		"tier30Full", utils.FormatInt(tier30.TotalDepositAmount()),
296		"tier30Left", utils.FormatInt(getCalculatedLeftReward(tier30)),
297		"tier90Full", utils.FormatInt(tier90.TotalDepositAmount()),
298		"tier90Left", utils.FormatInt(getCalculatedLeftReward(tier90)),
299		"tier180Full", utils.FormatInt(tier180.TotalDepositAmount()),
300		"tier180Left", utils.FormatInt(getCalculatedLeftReward(tier180)),
301		"currentHeight", utils.FormatInt(currentHeight),
302		"currentTime", utils.FormatInt(currentTime),
303	)
304
305	return projectLeftReward
306}
307
308// transferLeftFromProject transfers the remaining rewards of a project to a specified recipient.
309// This function is called by an admin to transfer any unclaimed rewards from a project to a recipient address.
310// It validates the project ID, checks the recipient conditions, calculates the remaining rewards, and performs the transfer.
311// Returns the amount of rewards transferred to the recipient and any error.
312func (lp *launchpadV1) transferLeftFromProject(_ int, rlm realm, project *launchpad.Project, recipient address, currentTime int64) (int64, error) {
313	if err := validateRefundProject(project, recipient, currentTime); err != nil {
314		return 0, err
315	}
316
317	emission.MintAndDistributeGns(cross(rlm))
318
319	accumTotalDistributeAmount := int64(0)
320	accumLeftReward := int64(0)
321	accumCollectedReward := int64(0)
322	accumCollectableReward := int64(0)
323
324	for _, tier := range project.Tiers() {
325		// Calculate pending rewards for remaining depositors
326		currentDepositCount := getTierCurrentDepositCount(tier)
327
328		if currentDepositCount > 0 {
329			claimableReward, err := lp.applyTierRewardGrowthWithRewards(tier, currentTime)
330			if err != nil {
331				return 0, err
332			}
333
334			accumCollectableReward = gnsmath.SafeAddInt64(accumCollectableReward, claimableReward)
335		}
336
337		leftReward := getCalculatedLeftReward(tier)
338		accumLeftReward = gnsmath.SafeAddInt64(accumLeftReward, leftReward)
339		accumCollectedReward = gnsmath.SafeAddInt64(accumCollectedReward, tier.TotalCollectedAmount())
340		accumTotalDistributeAmount = gnsmath.SafeAddInt64(accumTotalDistributeAmount, tier.TotalDistributeAmount())
341	}
342
343	if accumLeftReward == 0 {
344		return 0, errors.New("project has no remaining amount")
345	}
346
347	actualTotalDistributeAmount := gnsmath.SafeAddInt64(accumCollectedReward, accumLeftReward)
348	if accumTotalDistributeAmount != actualTotalDistributeAmount {
349		return 0, errors.New(ufmt.Sprintf("accumTotalDistributeAmount(%d) != accumCollectedReward(%d)+accumLeftReward(%d)", accumTotalDistributeAmount, accumCollectedReward, accumLeftReward))
350	}
351
352	// Calculate refundable amount: project remaining minus claimable rewards for remaining depositors
353	projectLeftReward := accumLeftReward
354	refundableAmount := gnsmath.SafeSubInt64(projectLeftReward, accumCollectableReward)
355
356	if refundableAmount < 0 {
357		return 0, errors.New(ufmt.Sprintf("refundableAmount(%d) < 0", refundableAmount))
358	}
359
360	if refundableAmount > 0 {
361		common.SafeGRC20Transfer(cross(rlm), project.TokenPath(), recipient, refundableAmount)
362	}
363
364	return refundableAmount, nil
365}
366
367// applyTierRewardGrowthWithRewards calculates the total claimable rewards
368// for all active deposits in a specific tier.
369func (lp *launchpadV1) applyTierRewardGrowthWithRewards(tier *launchpad.ProjectTier, currentTime int64) (int64, error) {
370	rewardManager, err := lp.getProjectTierRewardManager(tier.ID())
371	if err != nil {
372		return 0, err
373	}
374
375	// Update reward accumulation to current time before calculating claimable.
376	err = updateRewardPerDepositX128(rewardManager, getTierCurrentDepositAmount(tier), currentTime)
377	if err != nil {
378		return 0, err
379	}
380
381	emitUpdateLaunchpadRewardAccumulation(tier.ID(), rewardManager, getTierCurrentDepositAmount(tier))
382
383	return calculateClaimableRewardsForActiveDeposits(rewardManager), nil
384}
385
386// validateTransferLeft validates the transfer of remaining tokens
387func validateRefundProject(project *launchpad.Project, recipient address, currentTime int64) error {
388	if !recipient.IsValid() {
389		return errors.New(ufmt.Sprintf("invalid recipient address(%s)", recipient.String()))
390	}
391
392	return validateRefundRemainingAmount(project, currentTime)
393}
394
395type createProjectParams struct {
396	name                  string
397	tokenPath             string
398	recipient             address
399	depositAmount         int64
400	conditionTokens       string
401	conditionAmounts      string
402	tier30Ratio           int64
403	tier90Ratio           int64
404	tier180Ratio          int64
405	startTime             int64
406	currentTime           int64
407	currentHeight         int64
408	minimumStartDelayTime int64
409}
410
411func (p *createProjectParams) validate() error {
412	if err := p.validateName(); err != nil {
413		return err
414	}
415
416	if err := p.validateTokenPath(); err != nil {
417		return err
418	}
419
420	if err := p.validateRecipient(); err != nil {
421		return err
422	}
423
424	if err := p.validateDepositAmount(); err != nil {
425		return err
426	}
427
428	if err := p.validateRatio(); err != nil {
429		return err
430	}
431
432	if err := p.validateStartTime(p.currentTime, p.minimumStartDelayTime); err != nil {
433		return err
434	}
435
436	if err := p.validateConditions(); err != nil {
437		return err
438	}
439
440	return nil
441}
442
443// validateName checks if the project name is valid.
444func (p *createProjectParams) validateName() error {
445	if p.name == "" {
446		return makeErrorWithDetails(errInvalidInput, "project name cannot be empty")
447	}
448
449	if len(p.name) > 100 {
450		return makeErrorWithDetails(errInvalidInput, "project name is too long")
451	}
452
453	return nil
454}
455
456// validateTokenPath validates the token path is not empty and is registered.
457func (p *createProjectParams) validateTokenPath() error {
458	if p.tokenPath == "" {
459		return makeErrorWithDetails(errInvalidInput, "tokenPath cannot be empty")
460	}
461
462	if err := common.IsRegistered(p.tokenPath); err != nil && !isGovernanceToken(p.tokenPath) {
463		return makeErrorWithDetails(errInvalidInput, ufmt.Sprintf("tokenPath(%s) not registered", p.tokenPath))
464	}
465
466	return nil
467}
468
469// validateRecipient checks if the recipient address is valid.
470func (p *createProjectParams) validateRecipient() error {
471	if !p.recipient.IsValid() {
472		return makeErrorWithDetails(errInvalidAddress, ufmt.Sprintf("recipient address(%s)", p.recipient.String()))
473	}
474
475	return nil
476}
477
478// validateDepositAmount ensures that the deposit amount is greater than zero.
479func (p *createProjectParams) validateDepositAmount() error {
480	if p.depositAmount == 0 {
481		return makeErrorWithDetails(errInvalidInput, "deposit amount cannot be 0")
482	}
483
484	if p.depositAmount < 0 {
485		return makeErrorWithDetails(errInvalidInput, "deposit amount cannot be negative")
486	}
487
488	return nil
489}
490
491// validateRatio checks if each tier ratio is non-negative and the sum equals 100.
492func (p *createProjectParams) validateRatio() error {
493	if p.tier30Ratio < 0 || p.tier90Ratio < 0 || p.tier180Ratio < 0 {
494		return makeErrorWithDetails(
495			errInvalidInput,
496			ufmt.Sprintf("tier ratios must be non-negative (30:%d, 90:%d, 180:%d)", p.tier30Ratio, p.tier90Ratio, p.tier180Ratio),
497		)
498	}
499
500	sum := p.tier30Ratio + p.tier90Ratio + p.tier180Ratio
501	if sum != 100 {
502		return makeErrorWithDetails(
503			errInvalidInput,
504			ufmt.Sprintf("invalid ratio, sum of all tiers(30:%d, 90:%d, 180:%d) should be 100", p.tier30Ratio, p.tier90Ratio, p.tier180Ratio),
505		)
506	}
507
508	return nil
509}
510
511// validateStartTime checks if the start time is available with minimum delay requirement.
512func (p *createProjectParams) validateStartTime(now int64, minimumStartDelayTime int64) error {
513	availableStartTime := now + minimumStartDelayTime
514
515	if p.startTime < availableStartTime {
516		return makeErrorWithDetails(errInvalidInput, ufmt.Sprintf("start time(%d) must be greater than now(%d)", p.startTime, availableStartTime))
517	}
518
519	return nil
520}
521
522func (p *createProjectParams) validateConditions() error {
523	if p.conditionTokens == "" && p.conditionAmounts == "" {
524		return nil
525	}
526
527	tokenPaths := strings.Split(p.conditionTokens, stringSplitterPad)
528	minimumAmounts := strings.Split(p.conditionAmounts, stringSplitterPad)
529
530	if len(tokenPaths) != len(minimumAmounts) {
531		return makeErrorWithDetails(errInvalidInput, "conditionTokens and conditionAmounts are not matched")
532	}
533	if len(tokenPaths) > maxProjectConditionCount {
534		return makeErrorWithDetails(errInvalidInput, ufmt.Sprintf("condition count(%d) exceeds maximum(%d)", len(tokenPaths), maxProjectConditionCount))
535	}
536
537	tokenPathMap := make(map[string]bool)
538
539	for _, tokenPath := range tokenPaths {
540		err := common.IsRegistered(tokenPath)
541		if err != nil && !isGovernanceToken(tokenPath) {
542			return makeErrorWithDetails(errInvalidInput, ufmt.Sprintf("tokenPath(%s) not registered", tokenPath))
543		}
544
545		if tokenPathMap[tokenPath] {
546			return makeErrorWithDetails(errInvalidInput, ufmt.Sprintf("tokenPath(%s) is duplicated", tokenPath))
547		}
548
549		tokenPathMap[tokenPath] = true
550	}
551
552	for _, amountStr := range minimumAmounts {
553		minimumAmount, err := strconv.ParseInt(amountStr, 10, 64)
554		if err != nil {
555			return makeErrorWithDetails(errInvalidInput, ufmt.Sprintf("invalid condition amount(%s)", amountStr))
556		}
557
558		if minimumAmount <= 0 {
559			return makeErrorWithDetails(errInvalidInput, ufmt.Sprintf("condition amount(%s) is not available", amountStr))
560		}
561	}
562
563	return nil
564}
565
566func isGovernanceToken(tokenPath string) bool {
567	return tokenPath == GOV_XGNS_PATH
568}