package emission import ( "chain" "time" ufmt "gno.land/p/nt/ufmt/v0" gnsmath "gno.land/p/gnoswap/gnsmath/v1" prbac "gno.land/p/gnoswap/rbac/v1" "gno.land/p/gnoswap/utils/v1" "gno.land/r/gnoswap/access/v1" "gno.land/r/gnoswap/gns" "gno.land/r/gnoswap/halt/v1" ) const ( _ int = iota LIQUIDITY_STAKER DEVOPS COMMUNITY_POOL GOV_STAKER ) var ( // Stores the percentage (in basis points) for each distribution target // 1 basis point = 0.01% // These percentages can be modified by admin or governance. distributionBpsPct map[int]int64 distributedToStaker int64 // can be cleared by staker contract distributedToDevOps int64 distributedToCommunityPool int64 distributedToGovStaker int64 // can be cleared by governance staker // Historical total distributions (never reset) accuDistributedToStaker int64 accuDistributedToDevOps int64 accuDistributedToCommunityPool int64 accuDistributedToGovStaker int64 ) // Initialize default distribution percentages: // - Liquidity Stakers: 75% // - DevOps: 20% // - Community Pool: 5% // - Governance Stakers: 0% // // ref: https://docs.gnoswap.io/gnoswap-token/emission func init() { distributionBpsPct = map[int]int64{ LIQUIDITY_STAKER: 7500, DEVOPS: 2000, COMMUNITY_POOL: 500, GOV_STAKER: 0, } } // ChangeDistributionPct changes distribution percentages for emission targets. // // This function redistributes how newly minted GNS tokens are allocated across // protocol components. Before applying new ratios, it distributes any accumulated // emissions using the current ratios, ensuring emissions are distributed according // to the ratios in effect when they were generated. This prevents retroactive // application of new ratios to past emissions. // // Parameters: // - cur: Current realm context; callers use cross(cur) when crossing into this realm. // - liquidityStakerPct: percentage for liquidity stakers in basis points (100 = 1%, 10000 = 100%). // - devOpsPct: percentage for DevOps in basis points (100 = 1%, 10000 = 100%). // - communityPoolPct: percentage for the community pool in basis points (100 = 1%, 10000 = 100%). // - govStakerPct: percentage for governance stakers in basis points (100 = 1%, 10000 = 100%). // // Requirements: // - Percentages must sum to exactly 10000 (100%). // - Each percentage must be between 0 and 10000 inclusive. // // Example: // // ChangeDistributionPct( // 7000, // 70% to liquidity stakers // 2000, // 20% to devops // 1000, // 10% to community pool // 0, // 0% to governance stakers // ) // // Only callable by admin or governance. func ChangeDistributionPct( cur realm, liquidityStakerPct int64, devOpsPct int64, communityPoolPct int64, govStakerPct int64, ) { halt.AssertIsNotHaltedEmission() caller := cur.Previous().Address() access.AssertIsAdminOrGovernance(caller) assertValidDistributionPct(liquidityStakerPct, devOpsPct, communityPoolPct, govStakerPct) // Distribute accumulated emissions with current ratios before changing ratios. // This prevents retroactive application of new ratios to emissions that occurred // under previous ratio configurations. MintAndDistributeGns(cur) currentTimestamp := time.Now().Unix() stakerRewardPerSecond := GetEmissionAmountPerSecondBy(currentTimestamp, liquidityStakerPct) govStakerRewardPerSecond := GetEmissionAmountPerSecondBy(currentTimestamp, govStakerPct) if onDistributionPctChangeCallback != nil { onDistributionPctChangeCallback(cross(cur), stakerRewardPerSecond) } changeDistributionPcts(liquidityStakerPct, devOpsPct, communityPoolPct, govStakerPct) previousRealm := cur.Previous() chain.Emit( "ChangeDistributionPct", "prevAddr", previousRealm.Address().String(), "prevRealm", previousRealm.PkgPath(), "liquidityStakerPct", utils.FormatInt(liquidityStakerPct), "devOpsPct", utils.FormatInt(devOpsPct), "communityPoolPct", utils.FormatInt(communityPoolPct), "govStakerPct", utils.FormatInt(govStakerPct), "stakerRewardPerSecond", utils.FormatInt(stakerRewardPerSecond), "govStakerRewardPerSecond", utils.FormatInt(govStakerRewardPerSecond), ) } // changeDistributionPcts updates the distribution percentages for all targets. func changeDistributionPcts(liquidityStakerPct, devOpsPct, communityPoolPct, govStakerPct int64) { setDistributionBpsPct(LIQUIDITY_STAKER, liquidityStakerPct) setDistributionBpsPct(DEVOPS, devOpsPct) setDistributionBpsPct(COMMUNITY_POOL, communityPoolPct) setDistributionBpsPct(GOV_STAKER, govStakerPct) } func calculateDistributableAmounts(amount int64) (map[int]int64, int64) { distributable := make(map[int]int64, 0) totalSent := int64(0) for target, pct := range distributionBpsPct { distAmount := calculateAmount(amount, pct) if distAmount == 0 { continue } distributable[target] = distAmount totalSent = gnsmath.SafeAddInt64(totalSent, distAmount) } leftAmount := gnsmath.SafeSubInt64(amount, totalSent) return distributable, leftAmount } // calculateAmount converts basis points to actual token amount. func calculateAmount(amount, bptPct int64) int64 { if amount < 0 || bptPct < 0 || bptPct > 10000 { panic("invalid amount or bptPct") } // More precise overflow prevention const maxInt64 = 9223372036854775807 if amount > maxInt64/10000 { panic("amount too large, would cause overflow") } // Additional safety check for zero division if bptPct == 0 { return 0 } return amount * bptPct / 10000 } func applyDistribution(targets map[int]int64) (map[address]int64, error) { amountByAddress := make(map[address]int64, 0) for target, amount := range targets { var addr address switch target { case LIQUIDITY_STAKER: distributedToStaker = gnsmath.SafeAddInt64(distributedToStaker, amount) accuDistributedToStaker = gnsmath.SafeAddInt64(accuDistributedToStaker, amount) addr = access.MustGetAddress(prbac.ROLE_STAKER.String()) case DEVOPS: distributedToDevOps = gnsmath.SafeAddInt64(distributedToDevOps, amount) accuDistributedToDevOps = gnsmath.SafeAddInt64(accuDistributedToDevOps, amount) addr = access.MustGetAddress(prbac.ROLE_DEVOPS.String()) case COMMUNITY_POOL: distributedToCommunityPool = gnsmath.SafeAddInt64(distributedToCommunityPool, amount) accuDistributedToCommunityPool = gnsmath.SafeAddInt64(accuDistributedToCommunityPool, amount) addr = access.MustGetAddress(prbac.ROLE_COMMUNITY_POOL.String()) case GOV_STAKER: distributedToGovStaker = gnsmath.SafeAddInt64(distributedToGovStaker, amount) accuDistributedToGovStaker = gnsmath.SafeAddInt64(accuDistributedToGovStaker, amount) addr = access.MustGetAddress(prbac.ROLE_GOV_STAKER.String()) default: return nil, makeErrorWithDetails( errInvalidEmissionTarget, ufmt.Sprintf("invalid target(%d)", target), ) } amountByAddress[addr] = gnsmath.SafeAddInt64(amountByAddress[addr], amount) } return amountByAddress, nil } func transferToTarget(_ int, rlm realm, targets map[address]int64) error { for address, amount := range targets { gns.Transfer(cross(rlm), address, amount) } return nil } // GetDistributionBpsPct returns the configured distribution percentage in basis points for a target. // // Parameters: // - target: distribution target constant whose configured share is queried. // // Returns: // - pct: configured target share in basis points (100 = 1%, 10000 = 100%). // - error: nil when the target exists in the initialized map; otherwise an invalid-target error. func GetDistributionBpsPct(target int) (int64, error) { if err := validateDistributionTarget(target); err != nil { return 0, err } if distributionBpsPct == nil { return 0, makeErrorWithDetails( errInvalidEmissionTarget, ufmt.Sprintf("distributionBpsPct is nil"), ) } pct, exist := distributionBpsPct[target] if !exist { return 0, makeErrorWithDetails( errInvalidEmissionTarget, ufmt.Sprintf("invalid target(%d)", target), ) } return pct, nil } // GetDistributedToStaker returns the pending GNS amount allocated to liquidity stakers. // // Returns: // - amount: pending liquidity-staker allocation in GNS base units since the last clear. func GetDistributedToStaker() int64 { return distributedToStaker } // GetDistributedToDevOps returns the GNS amount currently accumulated for DevOps. // // Returns: // - amount: accumulated DevOps allocation in GNS base units. func GetDistributedToDevOps() int64 { return distributedToDevOps } // GetDistributedToCommunityPool returns the GNS amount currently accumulated for the community pool. // // Returns: // - amount: accumulated community-pool allocation in GNS base units. func GetDistributedToCommunityPool() int64 { return distributedToCommunityPool } // GetDistributedToGovStaker returns the pending GNS amount allocated to governance stakers. // // Returns: // - amount: pending governance-staker allocation in GNS base units since the last clear. func GetDistributedToGovStaker() int64 { return distributedToGovStaker } // AccumulateDistributedInfo returns the current pending allocation for every distribution target. // // Returns: // - toStaker: pending GNS allocation for liquidity stakers, in token base units. // - toDevOps: accumulated GNS allocation for DevOps, in token base units. // - toCommunityPool: accumulated GNS allocation for the community pool, in token base units. // - toGovStaker: pending GNS allocation for governance stakers, in token base units. func AccumulateDistributedInfo() (toStaker, toDevOps, toCommunityPool, toGovStaker int64) { toStaker = GetDistributedToStaker() toDevOps = GetDistributedToDevOps() toCommunityPool = GetDistributedToCommunityPool() toGovStaker = GetDistributedToGovStaker() return } // GetAccuDistributedToStaker returns the total historical GNS amount allocated to liquidity stakers. // // Returns: // - amount: cumulative liquidity-staker allocation in GNS base units; this value is not cleared. func GetAccuDistributedToStaker() int64 { return accuDistributedToStaker } // GetAccuDistributedToDevOps returns the total historical GNS amount allocated to DevOps. // // Returns: // - amount: cumulative DevOps allocation in GNS base units; this value is not cleared. func GetAccuDistributedToDevOps() int64 { return accuDistributedToDevOps } // GetAccuDistributedToCommunityPool returns the total historical GNS amount allocated to the community pool. // // Returns: // - amount: cumulative community-pool allocation in GNS base units; this value is not cleared. func GetAccuDistributedToCommunityPool() int64 { return accuDistributedToCommunityPool } // GetAccuDistributedToGovStaker returns the total historical GNS amount allocated to governance stakers. // // Returns: // - amount: cumulative governance-staker allocation in GNS base units; this value is not cleared. func GetAccuDistributedToGovStaker() int64 { return accuDistributedToGovStaker } // GetEmissionAmountPerSecondBy returns the GNS emission rate per second for a timestamp and target share. // // Parameters: // - timestamp: Unix timestamp at which the base emission rate is queried. // - distributionPct: target share in basis points, from 0 through 10000 inclusive. // // Returns: // - amountPerSecond: target's GNS emission rate in token base units per second, or zero outside the schedule. func GetEmissionAmountPerSecondBy(timestamp, distributionPct int64) int64 { return calculateAmount(gns.GetEmissionAmountPerSecondByTimestamp(timestamp), distributionPct) } // GetStakerEmissionAmountPerSecond returns the current GNS emission rate allocated to liquidity stakers. // // Returns: // - amountPerSecond: current liquidity-staker emission rate in GNS base units per second. // - error: nil when the liquidity-staker distribution share is available; otherwise the target lookup error. func GetStakerEmissionAmountPerSecond() (int64, error) { currentTimestamp := time.Now().Unix() pct, err := GetDistributionBpsPct(LIQUIDITY_STAKER) if err != nil { return 0, err } return GetEmissionAmountPerSecondBy(currentTimestamp, pct), nil } // GetStakerEmissionAmountPerSecondInRange returns liquidity-staker emission-rate change points in a time range. // // Parameters: // - start: inclusive start Unix timestamp for the range. // - end: inclusive end Unix timestamp for the range. // // Returns: // - timestamps: ordered timestamps in the range where the base emission rate changes. // - amountsPerSecond: liquidity-staker GNS rates corresponding to timestamps, in token base units per second. // - error: nil when the liquidity-staker distribution share is available; otherwise the target lookup error. func GetStakerEmissionAmountPerSecondInRange(start, end int64) ([]int64, []int64, error) { gnsHalvingBlocks, gnsHalvingEmissions := gns.GetEmissionAmountPerSecondInRange(start, end) halvingBlocks := make([]int64, len(gnsHalvingBlocks)) halvingEmissions := make([]int64, len(gnsHalvingEmissions)) pct, err := GetDistributionBpsPct(LIQUIDITY_STAKER) if err != nil { return nil, nil, err } for i := range halvingBlocks { halvingBlocks[i] = gnsHalvingBlocks[i] // Applying staker ratio for past halving blocks halvingEmissions[i] = calculateAmount(gnsHalvingEmissions[i], pct) } return halvingBlocks, halvingEmissions, nil } // ClearDistributedToStaker resets the pending distribution amount for liquidity stakers. // // Parameters: // - cur: Current realm context; callers use cross(cur) when crossing into this realm. // // Only callable by staker contract. func ClearDistributedToStaker(cur realm) { caller := cur.Previous().Address() access.AssertIsStaker(caller) distributedToStaker = 0 } // ClearDistributedToGovStaker resets the pending distribution amount for governance stakers. // // Parameters: // - cur: Current realm context; callers use cross(cur) when crossing into this realm. // // Only callable by governance staker contract. func ClearDistributedToGovStaker(cur realm) { caller := cur.Previous().Address() access.AssertIsGovStaker(caller) distributedToGovStaker = 0 } // setDistributionBpsPct changes percentage of each target for how much GNS it will get by emission. // Creates new map if nil. func setDistributionBpsPct(target int, pct int64) { if distributionBpsPct == nil { distributionBpsPct = make(map[int]int64) } distributionBpsPct[target] = pct } // targetToStr converts target constant to string representation. func targetToStr(target int) string { switch target { case LIQUIDITY_STAKER: return "LIQUIDITY_STAKER" case DEVOPS: return "DEVOPS" case COMMUNITY_POOL: return "COMMUNITY_POOL" case GOV_STAKER: return "GOV_STAKER" default: return "UNKNOWN" } }