From 47669d51a1174654b9e7a337a8f5a27c791442e7 Mon Sep 17 00:00:00 2001 From: MikaelVallenet Date: Thu, 5 Sep 2024 16:20:51 +0200 Subject: [PATCH] feat(launchpad-grc20): add sale realm w/ creating and buying mechanism --- gno/r/launchpad_grc20/airdrop_grc20.gno | 19 +-- gno/r/launchpad_grc20/airdrop_grc20_test.gno | 4 +- gno/r/launchpad_grc20/render.gno | 2 +- gno/r/launchpad_grc20/sale_grc20.gno | 146 +++++++++++++++++++ gno/r/launchpad_grc20/sales_grc20.gno | 1 - 5 files changed, 152 insertions(+), 20 deletions(-) create mode 100644 gno/r/launchpad_grc20/sale_grc20.gno delete mode 100644 gno/r/launchpad_grc20/sales_grc20.gno diff --git a/gno/r/launchpad_grc20/airdrop_grc20.gno b/gno/r/launchpad_grc20/airdrop_grc20.gno index fe36682451..f9488c450b 100644 --- a/gno/r/launchpad_grc20/airdrop_grc20.gno +++ b/gno/r/launchpad_grc20/airdrop_grc20.gno @@ -58,7 +58,7 @@ func NewAirdrop(tokenName, merkleRoot string, amountPerAddr uint64, startTimesta return airdropID } -func Redeem(airdropID uint64, proofs []Node) { +func Claim(airdropID uint64, proofs []Node) { airdrop := mustGetAirdrop(airdropID) caller := std.PrevRealm().Addr() @@ -96,19 +96,6 @@ func (a *Airdrop) hasAlreadyClaimed(caller std.Address) bool { func (a *Airdrop) isOnGoing() bool { now := time.Now().Unix() - - // if startTimestamp and endTimestamp are both 0, the airdrop is always ongoing - if a.startTimestamp == 0 && a.endTimestamp == 0 { - return true - } - // if startTimestamp is 0, the airdrop is ongoing until endTimestamp - if a.startTimestamp == 0 { - return now < a.endTimestamp - } - - // if endTimestamp is 0, the airdrop is ongoing since startTimestamp - if a.endTimestamp == 0 { - return a.startTimestamp <= now - } - return a.startTimestamp <= now && now < a.endTimestamp + return (a.startTimestamp == 0 || a.startTimestamp <= now) && + (a.endTimestamp == 0 || now < a.endTimestamp) } diff --git a/gno/r/launchpad_grc20/airdrop_grc20_test.gno b/gno/r/launchpad_grc20/airdrop_grc20_test.gno index 79f7f69c18..b89e6c450b 100644 --- a/gno/r/launchpad_grc20/airdrop_grc20_test.gno +++ b/gno/r/launchpad_grc20/airdrop_grc20_test.gno @@ -6,7 +6,7 @@ import ( "testing" ) -func TestRedeem(t *testing.T) { +func TestClaim(t *testing.T) { leaves := []Hashable{ Leaf{[]byte("g126gx6p6d3da4ymef35ury6874j6kys044r7zlg")}, Leaf{[]byte("g1ld6uaykyugld4rnm63rcy7vju4zx23lufml3jv")}, @@ -17,7 +17,7 @@ func TestRedeem(t *testing.T) { NewToken("MikaelVallenetCoin", "MVC", "https://mikaelvallenet.com", 18, 1000000, 1000000000, true, true) airdropID := NewAirdrop("MikaelVallenetCoin", root, 100, 0, 0) std.TestSetOrigCaller(std.Address("g126gx6p6d3da4ymef35ury6874j6kys044r7zlg")) - Redeem(airdropID, proofs) + Claim(airdropID, proofs) token := mustGetToken("MikaelVallenetCoin") balance := token.Token().BalanceOf(std.Address("g126gx6p6d3da4ymef35ury6874j6kys044r7zlg")) if balance != 100 { diff --git a/gno/r/launchpad_grc20/render.gno b/gno/r/launchpad_grc20/render.gno index d406c6cd20..05180489b2 100644 --- a/gno/r/launchpad_grc20/render.gno +++ b/gno/r/launchpad_grc20/render.gno @@ -110,7 +110,7 @@ func renderAirdropDetailPage(res *mux.ResponseWriter, req *mux.Request) { res.Write(ufmt.Sprintf("### Merkle Root: %s\n", airdrop.merkleRoot)) res.Write(ufmt.Sprintf("### Total addresses claimed: %d\n\n", airdrop.alreadyClaimed.Size())) res.Write("## Claim Instructions\n") - res.Write("To try to claim your tokens, call the redeem function with the airdrop ID and the proof of your address.\n") + res.Write("To try to claim your tokens, call the claim function with the airdrop ID and the proof of your address.\n") res.Write("If you are eligible, you will receive the tokens in your wallet.\n") } diff --git a/gno/r/launchpad_grc20/sale_grc20.gno b/gno/r/launchpad_grc20/sale_grc20.gno new file mode 100644 index 0000000000..76de01b44c --- /dev/null +++ b/gno/r/launchpad_grc20/sale_grc20.gno @@ -0,0 +1,146 @@ +package launchpad_grc20 + +import ( + "std" + "time" + + "gno.land/p/demo/avl" + "gno.land/p/demo/ufmt" +) + +type Sale struct { + token *Token + startTimestamp int64 + endTimestamp int64 + pricePerToken uint64 + alreadySold uint64 + limitPerAddr uint64 + minGoal uint64 + maxGoal uint64 + + vault std.Address +} + +var ( + sales *avl.Tree // sale ID -> sale + nextSaleID uint64 +) + +func init() { + sales = avl.NewTree() + nextSaleID = 1 +} + +func NewSale(tokenName string, startTimestamp, endTimestamp int64, pricePerToken, limitPerAddr, minGoal, maxGoal uint64) uint64 { + // check if the caller is the owner of the token + token := mustGetToken(tokenName) + token.admin.AssertCallerIsOwner() + + // check that the token is mintable + if !token.allowMint { + panic("token is not mintable") + } + + now := time.Now().Unix() + if startTimestamp < now { + panic("start timestamp must be in the future") + } + + if startTimestamp >= endTimestamp { + panic("invalid timestamps, start must be before end") + } + + if minGoal > maxGoal { + panic("min goal must be less than max goal") + } + + if pricePerToken == 0 { + panic("price per token must be greater than 0") + } + + sale := Sale{ + token: token, + startTimestamp: startTimestamp, + endTimestamp: endTimestamp, + pricePerToken: pricePerToken, + limitPerAddr: limitPerAddr, + minGoal: minGoal, + maxGoal: maxGoal, + } + + saleID := nextSaleID + nextSaleID++ + + sales.Set(ufmt.Sprintf("%d", saleID), &sale) + + return saleID +} + +func Buy(saleID, amount uint64) { + buyer := std.PrevRealm().Addr() + sale := mustGetSale(saleID) + if !sale.isOnGoing() { + panic("sale is not ongoing") + } + + if amount == 0 { + panic("amount must be greater than 0") + } + + // TODO: HAVE TO CHECK WHAT THE BUYER ALREADY BOUGHT IN THE SALE + if amount > sale.limitPerAddr { + panic("amount exceeds limit per address") + } + + if sale.alreadySold+amount > sale.maxGoal { + panic("amount exceeds max goal of the sale") + } + + if sale.token.Token().BalanceOf(buyer) < amount*sale.pricePerToken { + panic("insufficient balance") + } + + sale.buy(buyer, amount) +} + +func Finalize() { +} + +func mustGetSale(saleID uint64) *Sale { + sale, exists := sales.Get(ufmt.Sprintf("%d", saleID)) + if !exists { + panic("sale not found") + } + return sale.(*Sale) +} + +func (s *Sale) isOnGoing() bool { + return s.startTimestamp <= time.Now().Unix() && (s.endTimestamp == 0 || time.Now().Unix() < s.endTimestamp) +} + +func (s *Sale) buy(buyer std.Address, amount uint64) { + if !s.isOnGoing() { + panic("sale is not ongoing") + } + + if amount == 0 { + panic("amount must be greater than 0") + } + + // TODO: HAVE TO CHECK WHAT THE BUYER ALREADY BOUGHT IN THE SALE + if amount > s.limitPerAddr { + panic("amount exceeds limit per address") + } + + if s.alreadySold+amount > s.maxGoal { + panic("amount exceeds max goal of the sale") + } + + if s.token.Token().BalanceOf(buyer) < amount*s.pricePerToken { + panic("insufficient balance") + } + + checkErr(s.token.Token().TransferFrom(buyer, std.Address("g1ld6uaykyugld4rnm63rcy7vju4zx23lufml3jv"), amount*s.pricePerToken)) + +} + diff --git a/gno/r/launchpad_grc20/sales_grc20.gno b/gno/r/launchpad_grc20/sales_grc20.gno deleted file mode 100644 index 7fc735db3d..0000000000 --- a/gno/r/launchpad_grc20/sales_grc20.gno +++ /dev/null @@ -1 +0,0 @@ -package launchpad_grc20