diff --git a/_deploy/r/gnoswap/gns/calculate_amount.gno b/_deploy/r/gnoswap/gns/calculate_amount.gno new file mode 100644 index 00000000..d4ca708c --- /dev/null +++ b/_deploy/r/gnoswap/gns/calculate_amount.gno @@ -0,0 +1,60 @@ +package gns + +// calculateAmountToMint calculates the amount of gns to mint +// It calculates the amount of gns to mint for each halving year for block range. +// It also handles the left emission amount if the current block range includes halving year end block. +func calculateAmountToMint(fromHeight, toHeight int64) uint64 { + fromYear := GetHalvingYearByHeight(fromHeight) + toYear := GetHalvingYearByHeight(toHeight) + + amountToMint := uint64(0) + + for i := fromYear; i <= toYear; i++ { + yearEndHeight := halvingYearBlock[i] + mintUntilHeight := yearEndHeight + + if toHeight < mintUntilHeight { + mintUntilHeight = toHeight + } + + // how many blocks to calculate + numBlock := uint64(mintUntilHeight-fromHeight) + 1 + + // amount of gns to mint for each block for current year + singleBlockAmount := GetAmountByHeight(yearEndHeight) + + // total amount of gns to mint for current year + amountToMint += singleBlockAmount * numBlock + + // if last block of halving year, handle left emission amount + if isLastBlockOfHalvingYear(mintUntilHeight) { + amountToMint += handleLeftEmissionAmount(i, amountToMint) + } + + // update halving year mint amount + halvingYearMintAmount[i] += amountToMint + + // update fromHeight for next year (if necessary) + fromHeight = mintUntilHeight + 1 + } + + return amountToMint +} + +// isLastBlockOfHalvingYear returns true if the current block is the last block of a halving year. +func isLastBlockOfHalvingYear(height int64) bool { + year := GetHalvingYearByHeight(height) + lastBlock := halvingYearBlock[year] + + return height == lastBlock +} + +// handleLeftEmissionAmount handles the left emission amount for a halving year. +// It calculates the left emission amount by subtracting the halving year mint amount from the halving year amount. +func handleLeftEmissionAmount(year int64, amount uint64) uint64 { + return halvingYearAmount[year] - halvingYearMintAmount[year] - amount +} + +func setLeftEmissionAmount(amount uint64) { + leftEmissionAmount = amount +} diff --git a/_deploy/r/gnoswap/gns/calculate_amount_test.gno b/_deploy/r/gnoswap/gns/calculate_amount_test.gno new file mode 100644 index 00000000..692ca1db --- /dev/null +++ b/_deploy/r/gnoswap/gns/calculate_amount_test.gno @@ -0,0 +1,97 @@ +package gns + +import ( + "fmt" + "testing" + + "gno.land/p/demo/uassert" +) + +func TestCalculateAmountToMint(t *testing.T) { + // back up previous data + prevHalvingYearAmount := halvingYearMintAmount + + t.Run("1 block for same year 01", func(t *testing.T) { + amount := calculateAmountToMint(10, 10) + uassert.Equal(t, amountPerBlockPerHalvingYear[1], amount) + }) + + t.Run("entire block for year 01 + 1 block for year 02", func(t *testing.T) { + amount := calculateAmountToMint(halvingYearBlock[1], halvingYearBlock[1]+2) + uassert.Equal(t, halvingYearAmount[1]+amountPerBlockPerHalvingYear[2], amount) + }) + + // restore previous data + halvingYearMintAmount = prevHalvingYearAmount +} + +func TestIsLastBlockOfHalvingYear(t *testing.T) { + tests := make([]struct { + name string + height int64 + want bool + }, 0, 24) + + for i := int64(1); i <= 12; i++ { + tests = append(tests, struct { + name string + height int64 + want bool + }{ + name: fmt.Sprintf("last block of halving year %d", i), + height: halvingYearBlock[i], + want: true, + }) + + tests = append(tests, struct { + name string + height int64 + want bool + }{ + name: fmt.Sprintf("not last block of halving year %d", i), + height: halvingYearBlock[i] - 1, + want: false, + }) + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + uassert.Equal(t, tt.want, isLastBlockOfHalvingYear(tt.height)) + }) + } +} + +func TestHandleLeftEmissionAmount(t *testing.T) { + tests := make([]struct { + name string + year int64 + amount uint64 + want uint64 + }, 0, 24) + + for i := int64(1); i <= 12; i++ { + tests = append(tests, struct { + name string + year int64 + amount uint64 + want uint64 + }{ + name: fmt.Sprintf("handle left emission amount for year %d, non minted", i), + year: i, + amount: 0, + want: halvingYearAmount[i], + }) + + tests = append(tests, struct { + name string + year int64 + amount uint64 + want uint64 + }{ + name: fmt.Sprintf("handle left emission amount for year %d, minted", i), + year: i, + amount: uint64(123456), + want: halvingYearAmount[i] - uint64(123456), + }) + } +} diff --git a/_deploy/r/gnoswap/gns/gno.mod b/_deploy/r/gnoswap/gns/gno.mod index 8c541067..67209d1d 100644 --- a/_deploy/r/gnoswap/gns/gno.mod +++ b/_deploy/r/gnoswap/gns/gno.mod @@ -1,13 +1 @@ module gno.land/r/gnoswap/v1/gns - -require ( - gno.land/p/demo/grc/grc20 v0.0.0-latest - gno.land/p/demo/json v0.0.0-latest - gno.land/p/demo/ownable v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest - gno.land/p/demo/users v0.0.0-latest - gno.land/r/demo/users v0.0.0-latest - gno.land/r/demo/grc20reg v0.0.0-latest - gno.land/r/gnoswap/v1/common v0.0.0-latest - gno.land/r/gnoswap/v1/consts v0.0.0-latest -) diff --git a/_deploy/r/gnoswap/gns/gns.gno b/_deploy/r/gnoswap/gns/gns.gno index 327ffb44..a6c3c44c 100644 --- a/_deploy/r/gnoswap/gns/gns.gno +++ b/_deploy/r/gnoswap/gns/gns.gno @@ -16,39 +16,78 @@ import ( "gno.land/r/gnoswap/v1/consts" ) -const MAXIMUM_SUPPLY = uint64(1_000_000_000_000_000) // 1B +const ( + MAXIMUM_SUPPLY = uint64(1_000_000_000_000_000) + INITIAL_MINT_AMOUNT = uint64(100_000_000_000_000) + MAX_EMISSION_AMOUNT = MAXIMUM_SUPPLY - INITIAL_MINT_AMOUNT +) + +var ( + lastMintedHeight = std.GetHeight() +) var ( - lastMintedHeight int64 - amountToEmission uint64 + leftEmissionAmount = MAX_EMISSION_AMOUNT ) var ( Token, privateLedger = grc20.NewToken("Gnoswap", "GNS", 6) UserTeller = Token.CallerTeller() - owner = ownable.NewWithAddress("g17290cwvmrapvp869xfnhhawa8sm9edpufzat7d") // ADMIN + owner = ownable.NewWithAddress(consts.ADMIN) ) func init() { - privateLedger.Mint(owner.Owner(), 100_000_000_000_000) // 100_000_000 GNS ≈ 0.1B + privateLedger.Mint(owner.Owner(), INITIAL_MINT_AMOUNT) getter := func() *grc20.Token { return Token } grc20reg.Register(getter, "") +} - amountToEmission = MAXIMUM_SUPPLY - uint64(100_000_000_000_000) +// MintedEmissionAmount returns the amount of GNS that has been minted by the emission contract. +// It does not include initial minted amount. +func MintedEmissionAmount() uint64 { + return TotalSupply() - INITIAL_MINT_AMOUNT +} - lastMintedHeight = std.GetHeight() +func Mint(address pusers.AddressOrName) uint64 { + common.IsHalted() + + caller := std.PrevRealm().Addr() + if err := common.EmissionOnly(caller); err != nil { + panic(err) + } + + lastMintedHeight := GetLastMintedHeight() + currentHeight := std.GetHeight() + + // skip minting process if gns for current block is already minted + if skipIfSameHeight(lastMintedHeight, currentHeight) { + return 0 + } + + // calculate gns amount to mint, and the mint to the target address + amountToMint := calculateAmountToMint(lastMintedHeight+1, currentHeight) + err := privateLedger.Mint(users.Resolve(address), amountToMint) + if err != nil { + panic(err.Error()) + } + + // update + setLastMintedHeight(currentHeight) + setLeftEmissionAmount(leftEmissionAmount - amountToMint) + + return amountToMint } -func GetAmountToEmission() uint64 { return amountToEmission } +func Burn(from pusers.AddressOrName, amount uint64) { + owner.AssertCallerIsOwner() + fromAddr := users.Resolve(from) + checkErr(privateLedger.Burn(fromAddr, amount)) +} func TotalSupply() uint64 { return UserTeller.TotalSupply() } -func TotalMinted() uint64 { - return UserTeller.TotalSupply() - uint64(100_000_000_000_000) -} - func BalanceOf(owner pusers.AddressOrName) uint64 { ownerAddr := users.Resolve(owner) return UserTeller.BalanceOf(ownerAddr) @@ -93,93 +132,34 @@ func Render(path string) string { } } -// Mint mints GNS to the address. -// Only emission contract can call Mint. -func Mint(address pusers.AddressOrName) uint64 { - common.IsHalted() - - caller := std.PrevRealm().Addr() - if caller != consts.EMISSION_ADDR { - panic(addDetailToError( - errNoPermission, - ufmt.Sprintf("only emission contract(%s) can call Mint, called from %s", consts.EMISSION_ADDR, caller.String()), - )) - } - - // if not yet initialized, mint 0 amount - if initialized == false { - return 0 - } - - // calculate gns emission amount for every block, and send by single call - // for this case, we assume that inside of block range gnoswap state hasn't changed. - nowHeight := std.GetHeight() - amountToMint := uint64(0) - - if lastMintedHeight >= nowHeight { - return 0 - } - - // If from, to block is at same halving year, no need iterate - fromYear := GetHalvingYearByHeight(lastMintedHeight + 1) - toYear := GetHalvingYearByHeight(nowHeight) - - if fromYear == toYear { - numBlock := uint64(nowHeight - lastMintedHeight) - singleBlockAmount := GetAmountByHeight(nowHeight) - totalBlockAmount := singleBlockAmount * numBlock - - amountToMint += totalBlockAmount - amountToMint = checkAndHandleIfLastBlockOfHalvingYear(nowHeight, amountToMint) - - halvingYearMintAmount[fromYear] += totalBlockAmount - } else { - for i := lastMintedHeight + 1; i <= nowHeight; i++ { - amount := GetAmountByHeight(i) - amount = checkAndHandleIfLastBlockOfHalvingYear(i, amount) - year := GetHalvingYearByHeight(i) - halvingYearMintAmount[year] += amount - amountToMint += amount - } - } - - err := privateLedger.Mint(users.Resolve(address), amountToMint) +func checkErr(err error) { if err != nil { panic(err.Error()) } +} - lastMintedHeight = nowHeight - - return amountToMint +func isInitialized() bool { + return initialized } -func Burn(from pusers.AddressOrName, amount uint64) { - owner.AssertCallerIsOwner() - fromAddr := users.Resolve(from) - checkErr(privateLedger.Burn(fromAddr, amount)) +func GetLastMintedHeight() int64 { + return lastMintedHeight } -func checkAndHandleIfLastBlockOfHalvingYear(height int64, amount uint64) uint64 { - year := GetHalvingYearByHeight(height) - lastBlock := halvingYearBlock[year] - if height == lastBlock { - leftForThisYear := halvingYearAmount[year] - halvingYearMintAmount[year] - amount = leftForThisYear - return amount - } +func setLastMintedHeight(height int64) { + lastMintedHeight = height +} - return amount +func GetLeftEmissionAmount() uint64 { + return leftEmissionAmount } -func checkErr(err error) { - if err != nil { - panic(err.Error()) +// skipIfSameHeight returns true if the current block height is the same as the last minted height. +// This prevents multiple gns minting inside the same block. +func skipIfSameHeight(lastMintedHeight, currentHeight int64) bool { + if lastMintedHeight == currentHeight { + return true } -} -// TODO: -// 1. when emission contract mint gns reward, last executed height should be get from gns contract. -// mint function of gns contract and mintGns function of emission contract should be synchronized. -func GetLastMintedHeight() int64 { - return lastMintedHeight + return false } diff --git a/_deploy/r/gnoswap/gns/gns_test.gno b/_deploy/r/gnoswap/gns/gns_test.gno new file mode 100644 index 00000000..cab7e18d --- /dev/null +++ b/_deploy/r/gnoswap/gns/gns_test.gno @@ -0,0 +1,70 @@ +package gns + +import ( + "std" + "testing" + + "gno.land/p/demo/testutils" + "gno.land/p/demo/uassert" + + "gno.land/r/gnoswap/v1/consts" + + "gno.land/r/gnoswap/v1/gns" +) + +const ( + GNO_VM_DEFAULT_HEIGHT = int64(123) +) + +var ( + addr01 = testutils.TestAddress("addr01") + addr01Realm = std.NewUserRealm(addr01) + emissionRealm = std.NewCodeRealm(consts.EMISSION_PATH) +) + +func TestInitData(t *testing.T) { + t.Run("total supply", func(t *testing.T) { + uassert.Equal(t, INITIAL_MINT_AMOUNT, TotalSupply()) + }) + + t.Run("balance of admin", func(t *testing.T) { + uassert.Equal(t, INITIAL_MINT_AMOUNT, BalanceOf(a2u(consts.ADMIN))) + }) + + t.Run("last minted height", func(t *testing.T) { + uassert.Equal(t, GNO_VM_DEFAULT_HEIGHT, lastMintedHeight) // gnoVM(in test) default height + }) +} + +func TestMint(t *testing.T) { + t.Run("panic if caller is not emission", func(t *testing.T) { + std.TestSetRealm(addr01Realm) + uassert.PanicsWithMessage(t, + `caller(g1v9jxgu3sx9047h6lta047h6lta047h6l0js7st) has no permission`, + func() { Mint(a2u(addr01)) }) + }) + + t.Run("success mint", func(t *testing.T) { + std.TestSkipHeights(1) + + uassert.Equal(t, uint64(0), gns.BalanceOf(a2u(consts.EMISSION_ADDR))) + + std.TestSetRealm(emissionRealm) + uassert.Equal(t, amountPerBlockPerHalvingYear[1], Mint(a2u(consts.EMISSION_ADDR))) // 1 block for year 01 + + uassert.Equal(t, std.GetHeight(), GetLastMintedHeight()) + uassert.Equal(t, MAX_EMISSION_AMOUNT-amountPerBlockPerHalvingYear[1], GetLeftEmissionAmount()) + + uassert.Equal(t, amountPerBlockPerHalvingYear[1], gns.BalanceOf(a2u(consts.EMISSION_ADDR))) + }) +} + +func TestSkipIfSameHeight(t *testing.T) { + t.Run("should skip if height is same", func(t *testing.T) { + uassert.True(t, skipIfSameHeight(1, 1)) + }) + + t.Run("should not skip if height is different", func(t *testing.T) { + uassert.False(t, skipIfSameHeight(1, 2)) + }) +} diff --git a/_deploy/r/gnoswap/gns/halving.gno b/_deploy/r/gnoswap/gns/halving.gno index 9907be7b..b69a0bb6 100644 --- a/_deploy/r/gnoswap/gns/halving.gno +++ b/_deploy/r/gnoswap/gns/halving.gno @@ -153,7 +153,7 @@ func setAvgBlockTimeInMs(ms int64) { blockLeft := timeLeftMs / avgBlockTimeMs // how many reward left to next halving - minted := TotalMinted() + minted := MintedEmissionAmount() amountLeft := halvingYearAccuAmount[year] - minted // how much reward per block diff --git a/_deploy/r/gnoswap/gns/tests/gns_test.gnoA b/_deploy/r/gnoswap/gns/tests/gns_test.gnoA deleted file mode 100644 index 0b17cfc2..00000000 --- a/_deploy/r/gnoswap/gns/tests/gns_test.gnoA +++ /dev/null @@ -1,129 +0,0 @@ -package gns - -import ( - "std" - "testing" - - "gno.land/p/demo/testutils" - "gno.land/p/demo/uassert" - pusers "gno.land/p/demo/users" - - "gno.land/r/gnoswap/v1/consts" -) - -func TestMint(t *testing.T) { - t.Run("initial mint", func(t *testing.T) { - uassert.Equal(t, uint64(100_000_000_000_000), TotalSupply()) - uassert.Equal(t, int64(123), lastMintedHeight) - }) - - t.Run("panic if not emission", func(t *testing.T) { - uassert.PanicsWithMessage(t, - `[GNOSWAP-GNS-001] caller has no permission || only emission contract(g10xg6559w9e93zfttlhvdmaaa0er3zewcr7nh20) can call Mint, called from g1wymu47drhr0kuq2098m792lytgtj2nyx77yrsm`, - func() { Mint(pusers.AddressOrName(testutils.TestAddress("dummy"))) }) - }) - - t.Run("no block mined", func(t *testing.T) { - std.TestSetRealm(std.NewCodeRealm(consts.EMISSION_PATH)) - Mint(pusers.AddressOrName(consts.EMISSION_ADDR)) - - uassert.Equal(t, uint64(100_000_000_000_000), TotalSupply()) - uassert.Equal(t, int64(123), lastMintedHeight) - }) - - t.Run("1 block mined", func(t *testing.T) { - std.TestSkipHeights(1) - - std.TestSetRealm(std.NewCodeRealm(consts.EMISSION_PATH)) - Mint(pusers.AddressOrName(consts.EMISSION_ADDR)) - - uassert.Equal(t, uint64(100_000_000_000_000+14_269_406), TotalSupply()) - uassert.Equal(t, int64(124), lastMintedHeight) - }) - - t.Run("10 blocks mined", func(t *testing.T) { - std.TestSkipHeights(10) - - std.TestSetRealm(std.NewCodeRealm(consts.EMISSION_PATH)) - Mint(pusers.AddressOrName(consts.EMISSION_ADDR)) - - uassert.Equal(t, uint64(100000014269406+142_694_060), TotalSupply()) - uassert.Equal(t, int64(134), lastMintedHeight) - }) - - t.Run("reach first halving year", func(t *testing.T) { - height := std.GetHeight() - uassert.Equal(t, int64(134), height) - - year := GetHalvingYearByHeight(height) - uassert.Equal(t, int64(1), year) - - yearEndHeight := halvingYearBlock[year] - uassert.Equal(t, int64(15768123), yearEndHeight) - - leftBlock := yearEndHeight - height - uassert.Equal(t, int64(15767989), leftBlock) - - std.TestSkipHeights(15767980) // 9 block left to next halving year - - std.TestSetRealm(std.NewCodeRealm(consts.EMISSION_PATH)) - Mint(pusers.AddressOrName(consts.EMISSION_ADDR)) - - uassert.Equal(t, uint64(100000156963466+224_999_708_419_880), TotalSupply()) - // 324999865383346 - }) - - t.Run("year01 and year02", func(t *testing.T) { - height := std.GetHeight() - uassert.Equal(t, int64(15768114), height) - - year := GetHalvingYearByHeight(height) - uassert.Equal(t, int64(1), year) - - yearEndHeight := halvingYearBlock[year] - uassert.Equal(t, int64(15768123), yearEndHeight) - - leftBlock := yearEndHeight - height - uassert.Equal(t, int64(9), leftBlock) // 9 block left - - std.TestSetRealm(std.NewCodeRealm(consts.EMISSION_PATH)) - std.TestSkipHeights(9) // year 1 ends - std.TestSkipHeights(10) // year 2 starts and mined 10 blocks - - Mint(pusers.AddressOrName(consts.EMISSION_ADDR)) - - uassert.Equal(t, halvingYearMintAmount[1], uint64(225000000000000)) - uassert.Equal(t, halvingYearAmount[1], uint64(225000000000000)) - - uassert.Equal(t, TotalSupply(), uint64(325000142694060)) - // 325000142694060 - 324999865383346 - // minted: 277310714 - - // year 1 - // block 15768115: 14_269_406 - // block 15768116: 14_269_406 - // block 15768117: 14_269_406 - // block 15768118: 14_269_406 - // block 15768119: 14_269_406 - // block 15768120: 14_269_406 - // block 15768121: 14_269_406 - // block 15768122: 14_269_406 - // block 15768123: 14_269_406 + (left) 6_192_000 - // 128424654 + 6192000 = 134616654 - - // year 2 - // block 15768124: 14_269_406 - // block 15768125: 14_269_406 - // block 15768126: 14_269_406 - // block 15768127: 14_269_406 - // block 15768128: 14_269_406 - // block 15768129: 14_269_406 - // block 15768130: 14_269_406 - // block 15768131: 14_269_406 - // block 15768132: 14_269_406 - // block 15768133: 14_269_406 - // 142694060 - - // 134616654 + 142694060 = 277310714 - }) -} diff --git a/_deploy/r/gnoswap/gns/utils.gno b/_deploy/r/gnoswap/gns/utils.gno index 5948421e..9c0e7f38 100644 --- a/_deploy/r/gnoswap/gns/utils.gno +++ b/_deploy/r/gnoswap/gns/utils.gno @@ -2,9 +2,15 @@ package gns import ( "std" + + pusers "gno.land/p/demo/users" ) func getPrev() (string, string) { prev := std.PrevRealm() return prev.Addr().String(), prev.PkgPath() } + +func a2u(addr std.Address) pusers.AddressOrName { + return pusers.AddressOrName(addr) +}