diff --git a/_deploy/r/demo/gnoswap/consts_halving/consts.gno b/_deploy/r/demo/gnoswap/consts_halving/consts.gno index 8a4451f5..5987cd22 100644 --- a/_deploy/r/demo/gnoswap/consts_halving/consts.gno +++ b/_deploy/r/demo/gnoswap/consts_halving/consts.gno @@ -8,6 +8,7 @@ import ( const ( GNOSWAP_ADMIN std.Address = "g13f63ua8uhmuf9mgc0x8zfz04yrsaqh7j78vcgq" // GSA FEE_COLLECTOR std.Address = "g18sp3hq6zqfxw88ffgz773gvaqgzjhxy62l9906" // FCL + DEV_OPS std.Address = "g1mjvd83nnjee3z2g7683er55me9f09688pd4mj9" // DevOps INTERNAL_REWARD_ACCOUNT std.Address = "g1jms5fx2raq4qfkq3502mfh25g54nyl5qeuvz5y" // IRA for GNS BLOCK_GENERATION_INTERVAL int64 = 5 // 5 seconds @@ -91,5 +92,6 @@ const ( // ETCs const ( - ZERO_ADDRESS std.Address = "" + // REF: https://github.com/gnolang/gno/pull/2401#discussion_r1648064219 + ZERO_ADDRESS std.Address = "g100000000000000000000000000000000dnmcnx" ) diff --git a/_deploy/r/demo/gns_halving/gns.gno b/_deploy/r/demo/gns_halving/gns.gno index 3753c96d..ddd9ec1a 100644 --- a/_deploy/r/demo/gns_halving/gns.gno +++ b/_deploy/r/demo/gns_halving/gns.gno @@ -120,3 +120,13 @@ func Mint(address pusers.AddressOrName) { // println("height:", i, "minted:", amount) } } + +// XXX: Remove this +// ONLY FOR EMISSION TESTING +func TestSetLastMintedHeight(height int64) { + caller := std.PrevRealm().Addr() + if caller != consts.EMISSION_ADDR { + panic("only emission contract can call TestSetLastMintedHeight") + } + lastMintedHeight = height +} diff --git a/emission/__TEST_0_INIT_VARIABLE_AND_HELPER_test.gno b/emission/__TEST_0_INIT_VARIABLE_AND_HELPER_test.gno new file mode 100644 index 00000000..f9980b2d --- /dev/null +++ b/emission/__TEST_0_INIT_VARIABLE_AND_HELPER_test.gno @@ -0,0 +1,62 @@ +package emission + +import ( + "std" + "testing" + + "gno.land/r/demo/gns" + + "gno.land/r/demo/gnoswap/consts" + + pusers "gno.land/p/demo/users" +) + +var ( + gsa std.Address = consts.GNOSWAP_ADMIN +) + +// Realms to mock frames +var ( + gsaRealm = std.NewUserRealm(gsa) + govRealm = std.NewCodeRealm(consts.GOV_PATH) +) + +func gnsBalance(addr std.Address) uint64 { + a2u := pusers.AddressOrName(addr) + + return gns.BalanceOf(a2u) +} + +func shouldEQ(t *testing.T, got, expected interface{}) { + if got != expected { + t.Errorf("got %v, expected %v", got, expected) + } +} + +func shouldNEQ(t *testing.T, got, expected interface{}) { + if got == expected { + t.Errorf("got %v, didn't expected %v", got, expected) + } +} + +func shouldPanic(t *testing.T, f func()) { + defer func() { + if r := recover(); r == nil { + t.Errorf("expected panic") + } + }() + f() +} + +func shouldPanicWithMsg(t *testing.T, f func(), msg string) { + defer func() { + if r := recover(); r == nil { + t.Errorf("The code did not panic") + } else { + if r != msg { + t.Errorf("excepted panic(%v), got(%v)", msg, r) + } + } + }() + f() +} diff --git a/emission/__TEST_distribution_test.gno b/emission/__TEST_distribution_test.gno new file mode 100644 index 00000000..e0fdbb02 --- /dev/null +++ b/emission/__TEST_distribution_test.gno @@ -0,0 +1,69 @@ +package emission + +import ( + "std" + "testing" + + "gno.land/r/demo/gns" +) + +func TestEmitGns(t *testing.T) { + shouldEQ(t, gns.TotalSupply(), 100000000000000) // GSA has + shouldEQ(t, gnsBalance(emissionAddr), 0) + + EmitGns() // 1 ~ 123 height + + shouldEQ(t, gnsBalance(emissionAddr), 4387842345) + shouldEQ(t, gns.TotalSupply(), 100000000000000+4387842345) + + shouldEQ(t, std.GetHeight(), 123) +} + +func TestDistributeToTarget(t *testing.T) { + shouldEQ(t, gnsBalance(emissionAddr), 4387842345) + DistributeToTarget(gnsBalance(emissionAddr)) + shouldEQ(t, gnsBalance(emissionAddr), 1) // 1 left +} + +func TestDistributeToTargetAfter5Block(t *testing.T) { + std.TestSkipHeights(5) + EmitGns() + shouldEQ(t, gnsBalance(emissionAddr), 178367576) + + DistributeToTarget(gnsBalance(emissionAddr)) + shouldEQ(t, gnsBalance(emissionAddr), 1) // 1 left again +} + +func TestChangeDistributionPctByAdmin(t *testing.T) { + std.TestSetRealm(gsaRealm) + + shouldEQ(t, GetDistributionPct(LIQUIDITY_STAKING), 7500) + shouldEQ(t, GetDistributionPct(DEVOPS), 2000) + + ChangeDistributionPct02( + 1, 5000, + 2, 4500, + ) + shouldEQ(t, GetDistributionPct(LIQUIDITY_STAKING), 5000) + shouldEQ(t, GetDistributionPct(DEVOPS), 4500) + + ChangeDistributionPct03( + 1, 5000, + 2, 4000, + 3, 1000, + ) + shouldEQ(t, GetDistributionPct(LIQUIDITY_STAKING), 5000) + shouldEQ(t, GetDistributionPct(DEVOPS), 4000) + shouldEQ(t, GetDistributionPct(COMMUNITY_POOL), 1000) + + ChangeDistributionPct04( + 1, 10000, + 2, 0, + 3, 0, + 4, 0, + ) + shouldEQ(t, GetDistributionPct(LIQUIDITY_STAKING), 10000) + shouldEQ(t, GetDistributionPct(DEVOPS), 0) + shouldEQ(t, GetDistributionPct(COMMUNITY_POOL), 0) + shouldEQ(t, GetDistributionPct(XGNS), 0) +} diff --git a/emission/__TEST_emission_test.gnoA b/emission/__TEST_emission_test.gnoA new file mode 100644 index 00000000..77fd9bed --- /dev/null +++ b/emission/__TEST_emission_test.gnoA @@ -0,0 +1,41 @@ +package emission + +import ( + "std" + "testing" + + "gno.land/r/demo/gns" +) + +func TestEmitGns(t *testing.T) { + shouldEQ(t, gns.TotalSupply(), 100000000000000) // GSA has + shouldEQ(t, gnsBalance(emissionAddr), 0) + + EmitGns() // 1 ~ 123 height + + shouldEQ(t, gnsBalance(emissionAddr), 4387842345) + shouldEQ(t, gns.TotalSupply(), 100000000000000+4387842345) + + shouldEQ(t, std.GetHeight(), 123) +} + +func TestEmitGnsSameBlock(t *testing.T) { + // request mint again in same block => do not mint again + // it may happen because single block can have multiple txs & msgs + EmitGns() + shouldEQ(t, gns.TotalSupply(), 100000000000000+4387842345) +} + +func TestEmitGnsAllAmount(t *testing.T) { + std.TestSkipHeights(75686400 - 1) + gns.TestSetLastMintedHeight(std.GetHeight()) + std.TestSkipHeights(1) + + EmitGns() + shouldEQ(t, gns.TotalSupply(), 100000000000000+4387842345+2229594) + // all emission duration has been passed + + std.TestSkipHeights(1) + EmitGns() // since all emission has been done, no more minting + shouldEQ(t, gns.TotalSupply(), 100000000000000+4387842345+2229594) +} diff --git a/emission/distribution.gno b/emission/distribution.gno new file mode 100644 index 00000000..b12680b9 --- /dev/null +++ b/emission/distribution.gno @@ -0,0 +1,154 @@ +package emission + +import ( + "std" + + "gno.land/r/demo/gnoswap/consts" + "gno.land/r/demo/gns" + + "gno.land/p/demo/ufmt" +) + +// emissionTarget represents different targets for token emission. +type emissionTarget int + +// distributionPctMap maps emission targets to their respective distribution percentages. +type distributionPctMap map[emissionTarget]uint64 + +const ( + LIQUIDITY_STAKING emissionTarget = iota + 1 + DEVOPS + COMMUNITY_POOL + XGNS +) + +// distributionPct defines the distribution percentages. +var distributionPct distributionPctMap = distributionPctMap{ + LIQUIDITY_STAKING: 7500, // 75% + DEVOPS: 2000, // 20% + COMMUNITY_POOL: 500, // 5% + XGNS: 0, // 0% +} + +// DistributeToTarget distributes the specified amount to different targets based on their percentages. +func DistributeToTarget(amount uint64) { + totalSent := uint64(0) + for target, pct := range distributionPct { + distAmount := calculateAmount(amount, pct) + totalSent += distAmount + + transferToTarget(target, distAmount) + } + + // `amount-totalSent` can be left due to rounding + // it will be distributed next time +} + +// GetDistributionPct returns the distribution percentage for the given target. +func GetDistributionPct(target emissionTarget) uint64 { + return distributionPct[target] +} + +// ChangeDistributionPct01 changes the distribution percentage for the given single target. +func ChangeDistributionPct01(target01 emissionTarget, pct01 uint64) { + changeDistributionPct(target01, pct01) + + checkSumDistributionPct() +} + +// ChangeDistributionPct02 changes the distribution percentage for the given two targets. +func ChangeDistributionPct02( + target01 emissionTarget, pct01 uint64, + target02 emissionTarget, pct02 uint64, +) { + changeDistributionPct(target01, pct01) + changeDistributionPct(target02, pct02) + + checkSumDistributionPct() +} + +// ChangeDistributionPct03 changes the distribution percentage for the given three targets. +func ChangeDistributionPct03( + target01 emissionTarget, pct01 uint64, + target02 emissionTarget, pct02 uint64, + target03 emissionTarget, pct03 uint64, +) { + changeDistributionPct(target01, pct01) + changeDistributionPct(target02, pct02) + changeDistributionPct(target03, pct03) + + checkSumDistributionPct() +} + +// ChangeDistributionPct04 changes the distribution percentage for the given four targets. +func ChangeDistributionPct04( + target01 emissionTarget, pct01 uint64, + target02 emissionTarget, pct02 uint64, + target03 emissionTarget, pct03 uint64, + target04 emissionTarget, pct04 uint64, +) { + changeDistributionPct(target01, pct01) + changeDistributionPct(target02, pct02) + changeDistributionPct(target03, pct03) + changeDistributionPct(target04, pct04) + + checkSumDistributionPct() +} + +// calculateAmount calculates the amount based on the given percentage in basis points. +func calculateAmount(amount, bptPct uint64) uint64 { + return amount * bptPct / 10000 +} + +// transferToTarget transfers the specified amount to the given addresses. +func transferToTarget(target emissionTarget, amount uint64) { + switch target { + case LIQUIDITY_STAKING: + // transfer to staker contract + gns.Transfer(a2u(consts.STAKER_ADDR), amount) + case DEVOPS: + // transfer to devops + gns.Transfer(a2u(consts.DEV_OPS), amount) + case COMMUNITY_POOL: + // TBD, transfer to community pool + gns.Transfer(a2u(consts.ZERO_ADDRESS), amount) + case XGNS: + // TBD, transfer to xGNS + gns.Transfer(a2u(consts.ZERO_ADDRESS), amount) + default: + panic("invalid target") + } +} + +// changeDistributionPct changes the distribution percentage for the given target. +func changeDistributionPct(target emissionTarget, pct uint64) { + // only admin or governance can change + caller := std.PrevRealm().Addr() + if caller != consts.GNOSWAP_ADMIN && caller != consts.GOV_ADDR { + panic("only admin or governance can change distribution percentages") + } + + // cannot add new target + if target != LIQUIDITY_STAKING && target != DEVOPS && target != COMMUNITY_POOL && target != XGNS { + panic("invalid target") + } + + // Maximum pct for a single target is 10000 basis points (100%) + if pct > 10000 { + panic("percentage too high") + } + + distributionPct[target] = pct +} + +// checkSumDistributionPct ensures the sum of all distribution percentages is 100% +func checkSumDistributionPct() { + sum := uint64(0) + for _, pct := range distributionPct { + sum += pct + } + + if sum != 10000 { + panic(ufmt.Sprintf("sum of all pct should be 100%% (10000 bps), got %d\n", sum)) + } +} diff --git a/emission/emission.gno b/emission/emission.gno new file mode 100644 index 00000000..7a0759ed --- /dev/null +++ b/emission/emission.gno @@ -0,0 +1,14 @@ +package emission + +import ( + "std" + + "gno.land/r/demo/gnoswap/consts" + "gno.land/r/demo/gns" +) + +var emissionAddr std.Address = consts.EMISSION_ADDR + +func EmitGns() { + gns.Mint(a2u(emissionAddr)) +} diff --git a/emission/utils.gno b/emission/utils.gno new file mode 100644 index 00000000..c92e5cc5 --- /dev/null +++ b/emission/utils.gno @@ -0,0 +1,11 @@ +package emission + +import ( + "std" + + pusers "gno.land/p/demo/users" +) + +func a2u(addr std.Address) pusers.AddressOrName { + return pusers.AddressOrName(addr) +}