launchpad_deposit.gno

package launchpad

import (
	"chain"
	"chain/runtime"
	"errors"
	"time"

	gnsmath "gno.land/p/gnoswap/gnsmath"
	"gno.land/p/gnoswap/utils"

	"gno.land/r/gnoswap/common"
	"gno.land/r/gnoswap/gns"
	gov_staker "gno.land/r/gnoswap/gov/staker"
	"gno.land/r/gnoswap/gov/xgns"
	"gno.land/r/gnoswap/halt"
	"gno.land/r/gnoswap/launchpad"
	"gno.land/r/gnoswap/referral"
)

// DepositGns deposits GNS tokens to a launchpad project tier.
//
// Parameters:
//   - targetProjectTierID: format "{projectId}:{tierType}"
//   - depositAmount: amount of GNS to deposit
//   - referrer: referral address (optional)
//
// Returns deposit ID.
func (lp *launchpadV1) DepositGns(_ int, rlm realm, targetProjectTierID string, depositAmount int64, referrer string) string {
	if !rlm.IsCurrent() {
		panic(errors.New(errSpoofedRealm))
	}

	halt.AssertIsNotHaltedLaunchpad()

	previousRealm := rlm.Previous()

	assertIsValidAmount(depositAmount)

	projectID, tierDuration := parseProjectTierID(targetProjectTierID)
	caller := previousRealm.Address()

	project, err := lp.getProject(projectID)
	if err != nil {
		panic(err.Error())
	}

	beforeTotalGNSStakedAmount := lp.store.GetTotalGNSStakedAmount()

	deposit, rewardState, isFirstDeposit, distributeAmountPerSecondX128, err := lp.depositGns(
		0,
		rlm,
		project,
		tierDuration,
		depositAmount,
		caller,
	)
	if err != nil {
		panic(err.Error())
	}

	afterTotalGNSStakedAmount := gnsmath.SafeAddInt64(beforeTotalGNSStakedAmount, depositAmount)
	if err := lp.store.SetTotalGNSStakedAmount(0, rlm, afterTotalGNSStakedAmount); err != nil {
		panic(err.Error())
	}

	actualReferrer := referral.TryRegister(cross(rlm), caller, referrer)

	if isFirstDeposit {
		chain.Emit(
			"FirstDepositForProjectTier",
			"prevAddr", previousRealm.Address().String(),
			"prevRealm", previousRealm.PkgPath(),
			"targetProjectTierId", targetProjectTierID,
			"amount", utils.FormatInt(depositAmount),
			"depositId", deposit.ID(),
			"claimableTime", utils.FormatInt(rewardState.ClaimableTime()),
			"tierAmountPerSecondX128", distributeAmountPerSecondX128,
			"totalGNSStakedAmount", utils.FormatInt(afterTotalGNSStakedAmount),
			"recipient", project.Recipient().String(),
		)
	}

	chain.Emit(
		"DepositGns",
		"prevAddr", previousRealm.Address().String(),
		"prevRealm", previousRealm.PkgPath(),
		"targetProjectTierId", targetProjectTierID,
		"amount", utils.FormatInt(depositAmount),
		"depositId", deposit.ID(),
		"claimableTime", utils.FormatInt(rewardState.ClaimableTime()),
		"referrer", actualReferrer,
		"totalGNSStakedAmount", utils.FormatInt(afterTotalGNSStakedAmount),
		"recipient", project.Recipient().String(),
	)

	launchpadAddress := rlm.Address()

	// stake governance token to the project
	err = lp.stakeGovernance(0, rlm, project.Recipient(), depositAmount, launchpadAddress, caller)
	if err != nil {
		panic(err.Error())
	}

	return deposit.ID()
}

// depositGns deposits GNS to a project tier.
func (lp *launchpadV1) depositGns(
	_ int,
	rlm realm,
	project *launchpad.Project,
	tierDuration int64,
	depositAmount int64,
	callerAddress address,
) (*launchpad.Deposit, *launchpad.RewardState, bool, string, error) {
	balanceOfFn := func(tokenPath string, caller address) int64 {
		if tokenPath == GOV_XGNS_PATH {
			return xgns.BalanceOf(caller)
		}

		return common.BalanceOf(tokenPath, caller)
	}

	err := checkProjectConditions(project, callerAddress, balanceOfFn)
	if err != nil {
		return nil, nil, false, "", err
	}

	projectTier, err := getProjectTier(project, tierDuration)
	if err != nil {
		return nil, nil, false, "", err
	}

	currentTime := time.Now().Unix()
	currentHeight := runtime.ChainHeight()

	if !projectTier.IsActivated(currentTime) {
		return nil, nil, false, "", makeErrorWithDetails(errInactiveProject, project.ID())
	}

	depositID := lp.nextDepositID()
	deposit := launchpad.NewDeposit(
		depositID,
		project.ID(),
		tierDuration,
		callerAddress,
		depositAmount,
		currentHeight,
		currentTime,
		projectTier.EndTime(),
	)

	// Get state and store deposit
	deposits := lp.store.GetDeposits()
	deposits.Set(depositID, deposit)

	rewardManager, err := lp.getProjectTierRewardManager(projectTier.ID())
	if err != nil {
		return nil, nil, false, "", err
	}

	isFirstDeposit := !isRewardManagerInitialized(rewardManager)

	// update rewards before adding deposit to reward manager
	err = updateRewardPerDepositX128(rewardManager, getTierCurrentDepositAmount(projectTier), currentTime)
	if err != nil {
		return nil, nil, false, "", err
	}

	// add reward state to reward manager
	rewardState := addRewardStateByDeposit(rewardManager, deposit)

	// update tier data after adding reward state to reward manager
	depositToTier(projectTier, deposit)
	project.SetTier(tierDuration, projectTier)

	// Save the modified state back
	if err := lp.store.SetDeposits(0, rlm, deposits); err != nil {
		return nil, nil, false, "", err
	}

	emitUpdateLaunchpadRewardAccumulation(projectTier.ID(), rewardManager, getTierCurrentDepositAmount(projectTier))

	return deposit,
		rewardState,
		isFirstDeposit,
		rewardManager.DistributeAmountPerSecondX128().ToString(),
		nil
}

// stakeGovernance stakes governance token to the project.
func (lp *launchpadV1) stakeGovernance(_ int, rlm realm, recipient address, depositAmount int64, launchpadAddress address, callerAddress address) error {
	gov_staker.SetAmountByProjectWallet(cross(rlm), recipient, depositAmount, true)

	gns.TransferFrom(
		cross(rlm),
		callerAddress,
		launchpadAddress,
		depositAmount,
	)

	return nil
}