Transaction
552DDEC7F9BDE7…14F5AD5C7562
Block 77,254 · index 0 · indexed
Summary
- Hash
- 552DDEC7F9BDE7711D4A50ABD1F3B9424EAB5EDF54A38D7C28D014F5AD5C7562
- Block
- 77,254
- Size
- 41948 bytes
- Gas used
- 52,320,295 / 62,784,318
- Fee
- 62785ugnot
- Status
- success
Messages
- Package
- gno.land/r/gnoswap/emission
Arguments · 23
- #1emission
- #2README.md
- #3# Emission GNS token emission and distribution system. ## Overview The emission system controls creation and distribution of new GNS tokens with a deflationary model featuring periodic halvings, ensuring predictable and decreasing supply growth over 12 years. For more details, check out [docs](https://docs.gnoswap.io/gnoswap-token/emission). ## Token Economics - **Total Supply Cap**: 1,000,000,000 GNS - **Initial Minted**: 100,000,000 GNS, pre-minted to the configured `ADMIN` role address during GNS realm initialization - **To Be Minted**: 900,000,000 GNS over 12 years - **Halving Period**: Every 2 years (63,072,000 seconds) - **Halving Reduction**: 50% decrease in emission rate - **Distribution**: Automatic during protocol activity ## Configuration - **Distribution Ratios** (modifiable by admin or governance): - Liquidity Staker: 75% (default) - DevOps: 20% (default) - Community Pool: 5% (default) - Governance Staker: 0% (default) - **Start Time**: Unix timestamp. It may be changed while the configured timestamp is still in the future; once active, it cannot be changed. ## Core Features ### Emission Schedule Implements Bitcoin-style halving model: - Year 1-2: 100% emission rate - Year 3-4: 50% emission rate - Year 5-6: 25% emission rate - Year 7-8: 12.5% emission rate - Year 9-12: 6.25% emission rate ### Distribution Mechanism When triggered by protocol activity: 1. Calculates elapsed time since last distribution 2. Mints GNS based on the current timestamp range and halving-year rates 3. Distributes to targets per configured ratios 4. Carries forward any undistributed amounts If emission is halted, `MintAndDistributeGns` returns `(0, false)` without panicking. A caller that requires emission must explicitly handle that result. ## Key Functions ### `MintAndDistributeGns` Mints and distributes GNS tokens automatically. ### `SetDistributionStartTime` Sets or reschedules the emission start timestamp before distribution is active. The timestamp must be positive and in the future; after the configured start time has been reached, the timestamp is immutable. ### `ChangeDistributionPct` Updates distribution percentages (admin or governance only). ### `GetDistributionBpsPct` Returns current distribution percentage in basis points for a target, or an error if the target is invalid. ## Gnoweb `Render("")` shows distribution dates in UTC, GNS amounts with six decimal places, allocation percentages, and cumulative distributions for the four recipients. Staker and governance-staker counters since their last accounting clear are shown separately: these allocations have already been transferred and are not wallet balances or user-claimable rewards. Unsupported paths return `404`. ## Technical Details ### Timestamp-Based Emission The following is a conceptual view of the schedule: ``` emissionPerSecond = baseEmission / (2^halvingCount) amountToMint = emissionPerSecond * elapsedSeconds ``` The implementation uses integer, piecewise rates. For each halving year intersecting the inclusive mint range `[fromTimestamp, toTimestamp]`, it initializes `yearAmountPerSecond` as `floor(yearDistributionAmount / SECONDS_IN_YEAR)` and multiplies that rate by the inclusive number of seconds. The mint range is clamped to the 12-year schedule end. When a range reaches a year end, the remaining integer amount (including division dust) is added so that the year's allocation is exhausted. ### Halving Calculation Halving years are determined by the schedule's year boundaries; the conceptual form is: ``` halvingCount = floor(timeSinceStart / halvingPeriod) ``` ### Distribution Targets 1. **Liquidity Staker**: Rewards for LP providers 2. **DevOps**: Development and operations fund 3. **Community Pool**: Community-governed treasury 4. **Governance Staker**: GNS staking rewards (currently 0%) ## Usage ```go // Set emission start (admin/governance; timestamp must be in the future) SetDistributionStartTime(cross(cur), futureStartTimestamp) // Trigger emission (called automatically by protocol flows) amount, ok := MintAndDistributeGns(cross(cur)) // Update distribution ratios ChangeDistributionPct( cross(cur), 7000, // 70% to liquidity stakers 2000, // 20% to devops 1000, // 10% to community pool 0, // 0% to governance stakers ) // Query distribution info stakerPct, err := GetDistributionBpsPct(LIQUIDITY_STAKER) if err != nil { panic(err) } accumulated := GetAccuDistributedToStaker() rate, err := GetStakerEmissionAmountPerSecond() if err != nil { panic(err) } ``` ## Security - Start time may be rescheduled while still in the future and is immutable once active - Distribution percentages must sum to 10000 (100%) - A halted `MintAndDistributeGns` call returns `false`; no automatic cross-module cascade occurs - If staker cache invalidation is required, keep the optional distribution-change callback registered - Leftover tracking carries undistributed amounts forward - Halving is enforced at protocol level
- #4assert.gno
- #5package emission import ( ufmt "gno.land/p/nt/ufmt/v0" ) func assertDefaultInitialPoolExists() { if defaultInitialPoolChecker == nil { panic(makeErrorWithDetails(errInvalidEmissionStart, "default initial pool checker is not registered")) } if !defaultInitialPoolChecker(DefaultInitialPoolTierPath) { panic(makeErrorWithDetails(errInvalidEmissionStart, "default initial pool does not exist: "+DefaultInitialPoolTierPath)) } } // validateDistributionTarget returns an error if the given distribution target is invalid. func validateDistributionTarget(target int) error { validTargets := map[int]bool{ LIQUIDITY_STAKER: false, DEVOPS: false, COMMUNITY_POOL: false, GOV_STAKER: false, } if _, ok := validTargets[target]; !ok { return makeErrorWithDetails( errInvalidEmissionTarget, ufmt.Sprintf("invalid target(%d)", target), ) } return nil } // assertValidDistributionTarget panics if the given distribution target is invalid. // Kept for internal invariant checks. func assertValidDistributionTarget(target int) { if err := validateDistributionTarget(target); err != nil { panic(err) } } // assertValidDistributionPct ensures the sum of all distribution percentages equals 10000 (100%). // Panics if the sum does not equal exactly 10000 basis points. func assertValidDistributionPct(liquidityStakerPct, devOpsPct, communityPoolPct, govStakerPct int64) { // Validate individual percentages are non-negative and reasonable percentages := []int64{liquidityStakerPct, devOpsPct, communityPoolPct, govStakerPct} for i, pct := range percentages { if pct < 0 { panic(makeErrorWithDetails( errInvalidEmissionPct, ufmt.Sprintf("percentage %d cannot be negative: %d", i+1, pct), )) } if pct > 10000 { panic(makeErrorWithDetails( errInvalidEmissionPct, ufmt.Sprintf("percentage %d cannot exceed 100%%: %d", i+1, pct), )) } } sum := liquidityStakerPct + devOpsPct + communityPoolPct + govStakerPct if sum != 10000 { panic(makeErrorWithDetails( errInvalidEmissionPct, ufmt.Sprintf("sum of percentages must be 10000, got %d", sum), )) } }
- #6distribution.gno
- #7package 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" } }
- #8doc.gno
- #9// Package emission manages GNS token emission and distribution for GnoSwap. // // The emission system controls creation and distribution of new GNS tokens // with a deflationary model featuring periodic halvings over 12 years. // // Emission Schedule: // - Year 1-2: 100% emission rate (225,000,000 GNS/year) // - Year 3-4: 50% emission rate (112,500,000 GNS/year) // - Year 5-6: 25% emission rate (56,250,000 GNS/year) // - Year 7-8: 12.5% emission rate (28,125,000 GNS/year) // - Year 9-12: 6.25% emission rate (14,062,500 GNS/year) // // Distribution Targets (configurable via admin or governance): // - LIQUIDITY_STAKER: Rewards for LP providers (default 75%) // - DEVOPS: Development and operations fund (default 20%) // - COMMUNITY_POOL: Community treasury (default 5%) // - GOV_STAKER: GNS staking rewards (default 0%) // // Key Functions: // - MintAndDistributeGns: Mints and distributes GNS per emission schedule; // returns false only when emission is halted. // - SetDistributionStartTime: Sets or reschedules the start timestamp while // it is still in the future; the timestamp is immutable once active. // - ChangeDistributionPct: Updates distribution percentages // - ClearDistributedToStaker/GovStaker: Resets pending distribution amounts package emission
- #10emission.gno
- #11package emission import ( "chain" "chain/runtime" "math" "time" gnsmath "gno.land/p/gnoswap/gnsmath/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 ( totalDistributionDuration = 12 * 365 * 24 * 60 * 60 // 12 years // DefaultInitialPoolTierPath is the canonical default initial pool that must // exist before emission distribution can start. The pool contract registers a // checker (SetDefaultInitialPoolChecker) that verifies this pool exists. DefaultInitialPoolTierPath = "gno.land/r/gnoland/wugnot.wugnot:gno.land/r/gnoswap/gns.GNS:3000" ) var ( // leftGNSAmount tracks undistributed GNS tokens from previous distributions leftGNSAmount int64 // lastExecutedTimestamp stores the last timestamp when distribution was executed lastExecutedTimestamp int64 // emissionAddr is the address of the emission realm emissionAddr address // distributionStartTimestamp is the timestamp from which emission distribution starts // Default is 0, meaning distribution is not started until explicitly set distributionStartTimestamp int64 // onDistributionPctChangeCallback is called when distribution percentages change // This allows external contracts (like staker) to update their caches onDistributionPctChangeCallback func(cur realm, emissionAmountPerSecond int64) // defaultInitialPoolChecker verifies the canonical default initial pool exists. // It is registered by pool/v1 at initialization time so that emission does not // need a compile-time dependency on the pool realm. defaultInitialPoolChecker func(poolPath string) bool ) func init(cur realm) { emissionAddr = cur.Address() } // setLeftGNSAmount updates the undistributed GNS token amount func setLeftGNSAmount(amount int64) { if amount < 0 { panic("left GNS amount cannot be negative") } leftGNSAmount = amount } // setLastExecutedTimestamp updates the timestamp of the last emission distribution execution. func setLastExecutedTimestamp(timestamp int64) { if timestamp < 0 { panic("last executed timestamp cannot be negative") } lastExecutedTimestamp = timestamp } // MintAndDistributeGns mints and distributes GNS tokens according to the emission schedule. // // This function is called automatically by protocol contracts during user interactions // to trigger periodic GNS emission. It mints new tokens based on elapsed time since // last distribution and distributes them to predefined targets (staker, devops, etc.). // // Parameters: // - cur: Current realm context; callers use cross(cur) when crossing into this realm. // // Returns: // - distributedAmount: total amount of GNS distributed in this call // - success: false only when emission is halted; true when processing completes, even if no tokens are distributed // // Note: Distribution only occurs if start timestamp is set and reached. // Any undistributed tokens from previous calls are carried forward. func MintAndDistributeGns(cur realm) (int64, bool) { if halt.IsHaltedEmission() { return 0, false } currentHeight := runtime.ChainHeight() currentTimestamp := time.Now().Unix() // Check if distribution start timestamp is set and if current timestamp has reached it // If distributionStartTimestamp is 0 (default), skip distribution to prevent immediate start // If current timestamp is below start timestamp, skip distribution if distributionStartTimestamp == 0 || currentTimestamp < distributionStartTimestamp { return 0, true } // Skip if we've already minted tokens at this timestamp lastMintedTimestamp := gns.LastMintedTimestamp() if currentTimestamp <= lastMintedTimestamp { return 0, true } // Additional check to prevent re-entrancy if lastExecutedTimestamp >= currentTimestamp { // Skip if we've already processed this height in emission return 0, true } // Mint new tokens and add any leftover amounts from previous distribution mintedEmissionRewardAmount := gns.MintGns(cross(cur), emissionAddr) // Validate minted amount if mintedEmissionRewardAmount < 0 { panic("minted emission reward amount cannot be negative") } distributableAmount := mintedEmissionRewardAmount prevLeftAmount := GetLeftGNSAmount() if leftGNSAmount > 0 { // Check for overflow before addition if distributableAmount > math.MaxInt64-prevLeftAmount { panic("distributable amount would overflow") } distributableAmount += prevLeftAmount setLeftGNSAmount(0) } distributable, leftAmount := calculateDistributableAmounts(distributableAmount) totalDistAmount := gnsmath.SafeSubInt64(distributableAmount, leftAmount) if leftAmount > 0 { setLeftGNSAmount(leftAmount) } setLastExecutedTimestamp(currentTimestamp) amountByAddress, err := applyDistribution(distributable) if err != nil { panic(err) } if err := transferToTarget(0, cur, amountByAddress); err != nil { panic(err) } stakerPct, err := GetDistributionBpsPct(LIQUIDITY_STAKER) if err != nil { panic(err) } stakerRewardPerSecond := GetEmissionAmountPerSecondBy(currentTimestamp, stakerPct) govStakerPct, err := GetDistributionBpsPct(GOV_STAKER) if err != nil { panic(err) } govStakerRewardPerSecond := GetEmissionAmountPerSecondBy(currentTimestamp, govStakerPct) previousRealm := cur.Previous() chain.Emit( "MintAndDistributeGns", "prevAddr", previousRealm.Address().String(), "prevRealm", previousRealm.PkgPath(), "lastTimestamp", utils.FormatInt(lastExecutedTimestamp), "currentTimestamp", utils.FormatInt(currentTimestamp), "currentHeight", utils.FormatInt(currentHeight), "mintedAmount", utils.FormatInt(mintedEmissionRewardAmount), "prevLeftAmount", utils.FormatInt(prevLeftAmount), "distributedAmount", utils.FormatInt(totalDistAmount), "currentLeftAmount", utils.FormatInt(GetLeftGNSAmount()), "gnsTotalSupply", utils.FormatInt(gns.TotalSupply()), "stakerRewardPerSecond", utils.FormatInt(stakerRewardPerSecond), "govStakerRewardPerSecond", utils.FormatInt(govStakerRewardPerSecond), ) return totalDistAmount, true } // SetDistributionStartTime sets the timestamp when emission distribution starts. // // This function controls when GNS emission begins. Before the configured // timestamp is reached, an admin or governance caller may reschedule it. Once // the timestamp is reached, the start time is immutable. // // Parameters: // - cur: Current realm context; callers use cross(cur) when crossing into this realm. // - startTimestamp: positive future Unix timestamp when emission should begin. // // Requirements: // - Must be called before distribution starts. // - Timestamp must be in the future and leave room for the 12-year schedule. // // Effects: // - Sets global distribution start time. // - Initializes GNS emission state if not already started. // - Emission begins automatically when timestamp is reached. // // Only callable by admin or governance. func SetDistributionStartTime(cur realm, startTimestamp int64) { halt.AssertIsNotHaltedEmission() caller := cur.Previous().Address() access.AssertIsAdminOrGovernance(caller) assertDefaultInitialPoolExists() if startTimestamp <= 0 { panic("distribution start timestamp must be positive") } if startTimestamp > math.MaxInt64-totalDistributionDuration { panic("distribution end timestamp must be before max int64 timestamp") } currentTimestamp := time.Now().Unix() // Must be in the future. if startTimestamp <= currentTimestamp { panic("distribution start timestamp must be greater than current timestamp") } // Cannot change after distribution started. if distributionStartTimestamp != 0 && distributionStartTimestamp <= currentTimestamp { panic("distribution has already started, cannot change start timestamp") } prevStartTimestamp := distributionStartTimestamp if gns.MintedEmissionAmount() == 0 { currentHeight := runtime.ChainHeight() gns.InitEmissionState(cross(cur), currentHeight, startTimestamp) } distributionStartTimestamp = startTimestamp chain.Emit( "SetDistributionStartTime", "caller", caller.String(), "prevStartTimestamp", utils.FormatInt(prevStartTimestamp), "newStartTimestamp", utils.FormatInt(startTimestamp), "height", utils.FormatInt(runtime.ChainHeight()), "timestamp", utils.FormatInt(time.Now().Unix()), ) } // SetOnDistributionPctChangeCallback registers the optional callback invoked when // distribution percentages change. A nil callback clears the registration. // This allows external contracts (like staker) to update their internal caches // when governance or admin changes emission rates. // // Parameters: // - cur: Current realm context; callers use cross(cur) when crossing into this realm. // - callback: nil to clear the callback, or a function receiving its callback // realm context and the current staker GNS emission rate per second; a nonnil // callback is invoked immediately after registration. func SetOnDistributionPctChangeCallback(cur realm, callback func(cur realm, emissionAmountPerSecond int64)) { caller := cur.Previous().Address() access.AssertIsStaker(caller) if callback == nil { onDistributionPctChangeCallback = nil return } emissionAmountPerSecond, err := GetStakerEmissionAmountPerSecond() if err != nil { panic(err) } onDistributionPctChangeCallback = callback onDistributionPctChangeCallback(cross(cur), emissionAmountPerSecond) } // SetDefaultInitialPoolChecker registers the callback that verifies the // canonical default initial pool exists before emission starts. // // The checker receives the pool path as a parameter so emission owns the // canonical path policy (DefaultInitialPoolTierPath) while pool supplies only // the generic "does this pool exist" capability. // // Parameters: // - cur: Current realm context; callers use cross(cur) when crossing into this realm. // - checker: nonnil function returning true when the supplied pool path exists; // it is called with DefaultInitialPoolTierPath and nil causes a panic. // // Only callable by the pool contract. func SetDefaultInitialPoolChecker(cur realm, checker func(poolPath string) bool) { caller := cur.Previous().Address() access.AssertIsPool(caller) if checker == nil { panic(makeErrorWithDetails(errInvalidEmissionStart, "default initial pool checker cannot be nil")) } defaultInitialPoolChecker = checker }
- #12errors.gno
- #13package emission import ( ufmt "gno.land/p/nt/ufmt/v0" ) const ( errInvalidEmissionTarget = "[GNOSWAP-EMISSION-001] invalid emission target" errInvalidEmissionPct = "[GNOSWAP-EMISSION-002] invalid emission percentage" errDuplicateTarget = "[GNOSWAP-EMISSION-003] duplicate emission target" errDistributionAddressNotFound = "[GNOSWAP-EMISSION-004] distribution address not found" errInvalidEmissionStart = "[GNOSWAP-EMISSION-005] invalid emission start" ) // makeErrorWithDetails creates a new error by combining a base error with additional details. func makeErrorWithDetails(message string, detail string) error { return ufmt.Errorf("%s || %s", message, detail) }
- #14getter.gno
- #15package emission import ( gnsmath "gno.land/p/gnoswap/gnsmath/v1" ) // GetLeftGNSAmount returns the amount of undistributed GNS tokens from previous distributions. // // Returns: // - amount: undistributed GNS amount func GetLeftGNSAmount() int64 { return leftGNSAmount } // GetDistributionStartTimestamp returns the configured emission start timestamp. // The value may be in the future; it is 0 when no start timestamp is configured. // // Returns: // - timestamp: configured distribution start timestamp, or 0 if unconfigured func GetDistributionStartTimestamp() int64 { return distributionStartTimestamp } // GetLastExecutedTimestamp returns the timestamp of the last emission distribution execution. // // Returns: // - timestamp: last execution timestamp func GetLastExecutedTimestamp() int64 { return lastExecutedTimestamp } // GetAllDistributionBpsPct returns all distribution percentages in basis points. // // Returns: // - percentages: map of target to percentage in basis points func GetAllDistributionBpsPct() map[int]int64 { result := make(map[int]int64, len(distributionBpsPct)) if distributionBpsPct == nil { return result } for target, pct := range distributionBpsPct { result[target] = pct } return result } // GetTotalAccuDistributed returns the total accumulated distributed GNS amount. // // Returns: // - amount: total accumulated distributed GNS func GetTotalAccuDistributed() int64 { return gnsmath.SafeAddInt64( gnsmath.SafeAddInt64(accuDistributedToStaker, accuDistributedToDevOps), gnsmath.SafeAddInt64(accuDistributedToCommunityPool, accuDistributedToGovStaker), ) } // GetTotalDistributed returns the total pending distributed GNS amount. // // Returns: // - amount: total pending distributed GNS func GetTotalDistributed() int64 { return gnsmath.SafeAddInt64( gnsmath.SafeAddInt64(distributedToStaker, distributedToDevOps), gnsmath.SafeAddInt64(distributedToCommunityPool, distributedToGovStaker), ) } // GetDistributionEndTimestamp returns the timestamp when emission distribution ends. // // Returns: // - timestamp: distribution end timestamp, or 0 if not started func GetDistributionEndTimestamp() int64 { if distributionStartTimestamp == 0 { return 0 } return gnsmath.SafeAddInt64(distributionStartTimestamp, totalDistributionDuration-1) } // GetDistributableAmount returns distribution amounts by target and the remainder. // If timestamp is outside the distribution window, it returns an empty map and the full amount as left. // // Parameters: // - amount: total amount to distribute // - timestamp: timestamp to check distribution window // // Returns: // - distributions: map of target to distribution amount // - remainder: undistributed amount func GetDistributableAmount(amount, timestamp int64) (map[int]int64, int64) { if distributionStartTimestamp == 0 || timestamp < distributionStartTimestamp { return make(map[int]int64), amount } endTimestamp := GetDistributionEndTimestamp() if endTimestamp != 0 && timestamp > endTimestamp { return make(map[int]int64), amount } return calculateDistributableAmounts(amount) }
- #16gnomod.toml
- #17module = "gno.land/r/gnoswap/emission" gno = "0.9"
- #18render.gno
- #19package emission import ( "strings" "time" "gno.land/p/gnoswap/utils/v1" "gno.land/p/moul/md/v0" "gno.land/p/moul/mdtable/v0" ufmt "gno.land/p/nt/ufmt/v0" ) const gnsDisplayScale uint64 = 1_000_000 // Render returns the current GNS emission and distribution state. func Render(path string) string { if path != "" { return "404\n" } out := md.H1("GNS Emission") + "\n" + md.Paragraph("All GNS amounts use exactly six decimal places. Schedule timestamps are UTC.") scheduleTable := mdtable.Table{ Headers: []string{"Field", "Value"}, Rows: [][]string{ {"Distribution start", formatEmissionTimestamp(GetDistributionStartTimestamp(), "Not started")}, {"Distribution end", formatEmissionTimestamp(GetDistributionEndTimestamp(), "Not scheduled")}, {"Last execution", formatEmissionTimestamp(GetLastExecutedTimestamp(), "Not yet executed")}, }, } out += md.H2("Distribution schedule") + "\n" + scheduleTable.String() + "\n" accountingTable := mdtable.Table{ Headers: []string{"Metric", "Amount"}, Rows: [][]string{ {"Undistributed GNS", formatGnsAmount(GetLeftGNSAmount())}, {"Total cumulative distributed", formatGnsAmount(GetTotalAccuDistributed())}, }, } out += md.H2("Distribution accounting") + "\n" + accountingTable.String() + "\n" + md.Paragraph( "The cumulative total includes allocations sent to all four recipients;\n"+ "DevOps and community-pool rows represent completed direct-transfer accounting.", ) recipientsTable := mdtable.Table{ Headers: []string{"Recipient", "Allocation", "Cumulative distributed"}, Rows: [][]string{ {"Liquidity stakers", formatDistributionPercent(distributionBpsPct[LIQUIDITY_STAKER]), formatGnsAmount(GetAccuDistributedToStaker())}, {"DevOps", formatDistributionPercent(distributionBpsPct[DEVOPS]), formatGnsAmount(GetAccuDistributedToDevOps())}, {"Community pool", formatDistributionPercent(distributionBpsPct[COMMUNITY_POOL]), formatGnsAmount(GetAccuDistributedToCommunityPool())}, {"Governance stakers", formatDistributionPercent(distributionBpsPct[GOV_STAKER]), formatGnsAmount(GetAccuDistributedToGovStaker())}, }, } out += md.H2("Recipients") + "\n" + recipientsTable.String() + "\n" stakingTable := mdtable.Table{ Headers: []string{"Recipient", "Since last accounting clear"}, Rows: [][]string{ {"Liquidity stakers", formatGnsAmount(GetDistributedToStaker())}, {"Governance stakers", formatGnsAmount(GetDistributedToGovStaker())}, }, } out += md.H2("Staking accounting") + "\n" + md.Paragraph( "These allocations have already been transferred to the recipients.\n"+ "The counters below show amounts since each accounting contract's last clear; they\n"+ "are not wallet balances or claimable amounts.", ) + stakingTable.String() return out } func formatEmissionTimestamp(timestamp int64, zeroLabel string) string { if timestamp == 0 { return zeroLabel } return time.Unix(timestamp, 0).UTC().Format(time.RFC3339) } func formatGnsAmount(amount int64) string { sign := "" magnitude := uint64(amount) if amount < 0 { sign = "-" // Avoid overflowing when formatting the smallest int64 value. magnitude = uint64(-(amount + 1)) + 1 } whole := magnitude / gnsDisplayScale fraction := magnitude % gnsDisplayScale return ufmt.Sprintf("%s%d.%s GNS", sign, whole, zeroPadUnsigned(fraction, 6)) } func formatDistributionPercent(bps int64) string { sign := "" magnitude := uint64(bps) if bps < 0 { sign = "-" magnitude = uint64(-(bps + 1)) + 1 } whole := magnitude / 100 fraction := magnitude % 100 return ufmt.Sprintf("%s%d.%s%%", sign, whole, zeroPadUnsigned(fraction, 2)) } func zeroPadUnsigned(value uint64, width int) string { valueString := utils.FormatUint(value) padding := width - len(valueString) if padding <= 0 { return valueString } return strings.Repeat("0", padding) + valueString }
- #20utils.gno
- #21package emission
- #22/gno.MemPackageType
- #23 MPUserAll
Result log
msg:0,success:true,log:,events:[]