Search Apps Documentation Source Content File Folder Download Copy Actions Download

launchpad_project.gno

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