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}