diff --git a/gov/governance/governance_proposal.gno b/gov/governance/governance_proposal.gno index 94c52a35..4112892e 100644 --- a/gov/governance/governance_proposal.gno +++ b/gov/governance/governance_proposal.gno @@ -65,7 +65,7 @@ func ProposeText( Yea: u256.Zero(), Nay: u256.Zero(), ConfigVersion: uint64(len(configVersions)), // use latest config version - QuorumAmount: xgns.TotalSupply() * config.Quorum / 100, + QuorumAmount: xgns.VotingSupply() * config.Quorum / 100, Title: title, Description: description, } @@ -122,7 +122,7 @@ func ProposeCommunityPoolSpend( Yea: u256.Zero(), Nay: u256.Zero(), ConfigVersion: uint64(len(configVersions)), - QuorumAmount: xgns.TotalSupply() * config.Quorum / 100, + QuorumAmount: xgns.VotingSupply() * config.Quorum / 100, Title: title, Description: description, CommunityPoolSpend: CommunityPoolSpendInfo{ @@ -211,7 +211,7 @@ func ProposeParameterChange( Yea: u256.Zero(), Nay: u256.Zero(), ConfigVersion: uint64(len(configVersions)), - QuorumAmount: xgns.TotalSupply() * config.Quorum / 100, + QuorumAmount: xgns.VotingSupply() * config.Quorum / 100, Title: title, Description: description, Execution: ExecutionInfo{ diff --git a/gov/governance/tests/__TEST_0_INIT_TOKEN_REGISTER_test.gno b/gov/governance/tests/__TEST_0_INIT_TOKEN_REGISTER_test.gno index 2e733ae2..1c82e0fe 100644 --- a/gov/governance/tests/__TEST_0_INIT_TOKEN_REGISTER_test.gno +++ b/gov/governance/tests/__TEST_0_INIT_TOKEN_REGISTER_test.gno @@ -21,6 +21,7 @@ import ( cp "gno.land/r/gnoswap/v2/community_pool" gs "gno.land/r/gnoswap/v2/gov/staker" + lp "gno.land/r/gnoswap/v2/launchpad" pf "gno.land/r/gnoswap/v2/protocol_fee" pusers "gno.land/p/demo/users" @@ -163,4 +164,13 @@ func init() { gs.RegisterGRC20Interface("gno.land/r/demo/wugnot", WugnotToken{}) gs.RegisterGRC20Interface("gno.land/r/onbloc/obl", OBLToken{}) gs.RegisterGRC20Interface("gno.land/r/gnoswap/v2/gns", GNSToken{}) + + // LAUNCHPAD + lp.RegisterGRC20Interface("gno.land/r/onbloc/bar", BarToken{}) + lp.RegisterGRC20Interface("gno.land/r/onbloc/foo", FooToken{}) + lp.RegisterGRC20Interface("gno.land/r/onbloc/baz", BazToken{}) + lp.RegisterGRC20Interface("gno.land/r/onbloc/qux", QuxToken{}) + lp.RegisterGRC20Interface("gno.land/r/demo/wugnot", WugnotToken{}) + lp.RegisterGRC20Interface("gno.land/r/onbloc/obl", OBLToken{}) + lp.RegisterGRC20Interface("gno.land/r/gnoswap/v2/gns", GNSToken{}) } diff --git a/gov/governance/tests/__TEST_0_INIT_VARIABLE_AND_HELPER_test.gno b/gov/governance/tests/__TEST_0_INIT_VARIABLE_AND_HELPER_test.gno index a2a3e1a7..0de0a1a6 100644 --- a/gov/governance/tests/__TEST_0_INIT_VARIABLE_AND_HELPER_test.gno +++ b/gov/governance/tests/__TEST_0_INIT_VARIABLE_AND_HELPER_test.gno @@ -49,7 +49,7 @@ func ugnotBalanceOf(addr std.Address) uint64 { func shouldEQ(t *testing.T, got, expected interface{}) { if got != expected { - t.Errorf("got %v, expected %v", got, expected) + t.Errorf("got\n%v\n\nexpected\n%v\n", got, expected) } } diff --git a/gov/governance/tests/__TEST_governance_vote_without_launchpad_xgns_test.gno b/gov/governance/tests/__TEST_governance_vote_without_launchpad_xgns_test.gno new file mode 100644 index 00000000..b5f601ba --- /dev/null +++ b/gov/governance/tests/__TEST_governance_vote_without_launchpad_xgns_test.gno @@ -0,0 +1,482 @@ +package governance + +import ( + "std" + "strings" + "testing" + "time" + + "gno.land/p/demo/testutils" + + u256 "gno.land/p/gnoswap/uint256" + + "gno.land/r/gnoswap/v2/consts" + "gno.land/r/gnoswap/v2/gns" + "gno.land/r/gnoswap/v2/gov/xgns" + + gs "gno.land/r/gnoswap/v2/gov/staker" + lp "gno.land/r/gnoswap/v2/launchpad" + + // grc20 tokens + "gno.land/r/onbloc/obl" +) + +var ( + dummyToAddr = testutils.TestAddress("dummyTo") + dummyToRealm = std.NewUserRealm(dummyToAddr) + + dummyAddr = testutils.TestAddress("dummy") + dummyRealm = std.NewUserRealm(dummyAddr) + + reDelegate = testutils.TestAddress("reDelegate") + + // launchpad + projectAddr = testutils.TestAddress("projectAddr") + projectRealm = std.NewUserRealm(projectAddr) + + user01 = testutils.TestAddress("user01") + user01Realm = std.NewUserRealm(user01) +) + +func TestCheckInitialGnsAndXGns(t *testing.T) { + t.Run("check current gns and xgns (total/voting) supply", func(t *testing.T) { + gnsTotal := gns.TotalSupply() + gsaGnsBalance := gns.BalanceOf(a2u(gsa)) + xgnsTotal := xgns.TotalSupply() + xgnsVoting := xgns.VotingSupply() + shouldEQ(t, gnsTotal, uint64(100_000_000_000_000)) + shouldEQ(t, gsaGnsBalance, uint64(100_000_000_000_000)) + shouldEQ(t, xgnsTotal, uint64(0)) + shouldEQ(t, xgnsVoting, uint64(0)) + }) +} + +func TestLaunchPadCreateProject(t *testing.T) { + std.TestSetRealm(gsaRealm) + std.TestSetOrigCaller(gsa) + + obl.Approve(a2u(consts.LAUNCHPAD_ADDR), 1_000_000_000) + projectId := lp.CreateProject( + oblPath, + projectAddr, + uint64(1_000_000_000), // 1000000000 + "", + "", + uint64(10), // 100000000 + uint64(20), // 200000000 + uint64(70), // 700000000 + uint64(time.Now().Unix()+10), // 5 block later + ) + shouldEQ(t, projectId, `gno.land/r/onbloc/obl:123`) + std.TestSkipHeights(10) // active project +} + +func TestLaunchPadDeposit(t *testing.T) { + std.TestSetRealm(gsaRealm) + std.TestSetOrigCaller(gsa) + gns.Transfer(a2u(user01), uint64(1_000_000)) // to deposit + + std.TestSetRealm(user01Realm) + std.TestSetOrigCaller(user01) + gns.Approve(a2u(consts.LAUNCHPAD_ADDR), uint64(1_000_000)) + + lp.DepositGns( + `gno.land/r/onbloc/obl:123:30`, + uint64(1_000_000), // this xGNS can not be used for voting + ) + std.TestSkipHeights(1) +} + +func TestCheckXGns(t *testing.T) { + t.Run("check current gns and xgns (total/voting) supply (after launchpad project create and deposit)", func(t *testing.T) { + gnsTotal := gns.TotalSupply() + gsaGnsBalance := gns.BalanceOf(a2u(gsa)) + xgnsTotal := xgns.TotalSupply() + xgnsVoting := xgns.VotingSupply() + shouldEQ(t, gnsTotal, uint64(100_000_000_000_000)) + shouldEQ(t, gsaGnsBalance, uint64(99_999_999_000_000)) + shouldEQ(t, xgnsTotal, uint64(1_000_000)) + shouldEQ(t, xgnsVoting, uint64(0)) + }) +} + +func TestProposeText(t *testing.T) { + t.Run("ProposeText with insufficient delegation", func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Errorf("The code did not panic") + } else if !strings.Contains(string(r), "PROPOSER_HAS_NOT_ENOUGH_XGNS") { + t.Errorf("Unexpected panic message: %v", r) + } + }() + + ProposeText("title", "text") + }) + + t.Run("Successful text proposal", func(t *testing.T) { + proposalsJson := GetProposals() + if proposalsJson != `` { + t.Errorf("Expected empty proposals, got %v", proposalsJson) + } + + std.TestSetOrigCaller(gsa) + std.TestSetRealm(gsaRealm) + gns.Approve(a2u(consts.GOV_STAKER_ADDR), uint64(1_000_000)) + gs.Delegate(gsa, uint64(1_000_000)) + std.TestSkipHeights(11) // VotingWeightSmoothingDuration is 10 block + + proposalID := ProposeText("test_title", "test_description") + if proposalID != 1 { + t.Errorf("Expected proposal ID to be 1, got %d", proposalID) + } + + proposal, exist := proposals[proposalId] + if !exist { + t.Errorf("Proposal not found after creation") + } + + if proposal.Proposer != std.GetOrigCaller() { + t.Errorf("Incorrect proposer. Expected %v, got %v", std.GetOrigCaller(), proposal.Proposer) + } + + if proposal.ProposalType != "TEXT" { + t.Errorf("Incorrect proposal type. Expected TEXT, got %v", proposal.ProposalType) + } + + if !proposal.ExecutionState.Created { + t.Errorf("Proposal execute state(created) not set correctly") + } + + if !proposal.ExecutionState.Upcoming { + t.Errorf("Proposal execute state(upcoming) not set correctly") + } + + if proposal.Yea.Cmp(u256.NewUint(0)) != 0 || proposal.Nay.Cmp(u256.NewUint(0)) != 0 { + t.Errorf("Initial vote counts should be zero") + } + + if proposal.ConfigVersion != 1 { + t.Errorf("Initial config version should be 1") + } + + if proposal.Title != "test_title" { + t.Errorf("Incorrect title. Expected test_title, got %v", proposal.Title) + } + + if proposal.Description != "test_description" { + t.Errorf("Incorrect text. Expected test_description, got %v", proposal.Description) + } + + proposalsJson = GetProposals() + if proposalsJson != `{"height":"145","now":"1234567934","proposals":[{"id":"1","configVersion":"1","proposer":"g1lmvrrrr4er2us84h2732sru76c9zl2nvknha8c","status":"eyJDcmVhdGVkQXQiOiIxMjM0NTY3OTM0IiwiVXBjb21pbmciOiJ0cnVlIiwiQWN0aXZlIjoiZmFsc2UiLCJWb3RpbmdTdGFydCI6IjEyMzQ1Njc5NDQiLCJWb3RpbmdFbmQiOiIxMjM0NTY3OTc0IiwiUGFzc2VkIjoiZmFsc2UiLCJQYXNzZWRBdCI6IjAiLCJSZWplY3RlZCI6ImZhbHNlIiwiUmVqZWN0ZWRBdCI6IjAiLCJDYW5jZWxlZCI6ImZhbHNlIiwiQ2FuY2VsZWRBdCI6IjAiLCJFeGVjdXRlZCI6ImZhbHNlIiwiRXhlY3V0ZWRBdCI6IjAiLCJFeHBpcmVkIjoiZmFsc2UiLCJFeHBpcmVkQXQiOiIwIn0=","type":"TEXT","title":"test_title","description":"test_description","vote":"eyJxdW9ydW0iOiI1MDAwMDAiLCJtYXgiOiIxMDAwMDAwIiwieWVzIjoiMCIsIm5vIjoiMCJ9","extra":""}]}` { + t.Errorf("Incorrect proposals json: %v", proposalsJson) + } + + votesJson := GetVotesByAddress(gsa.String()) + if votesJson != `` { + t.Errorf("Expected empty votes, got %v", votesJson) + } + }) +} + +func TestVote(t *testing.T) { + t.Run("Vote non existent proposal", func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Errorf("The code did not panic") + } else if !strings.Contains(string(r), "does not exist") { + t.Errorf("Unexpected panic message: %v", r) + } + }() + + Vote(uint64(123), true) + }) + + proposalId := uint64(1) // text proposal id + + t.Run("Vote before voting period", func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Errorf("The code did not panic") + } else if !strings.Contains(string(r), "not started yet") { + t.Errorf("Unexpected panic message: %v", r) + } + }() + + Vote(proposalId, true) + }) + + t.Run("Successful vote YES", func(t *testing.T) { + std.TestSkipHeights(11) + + voteKey := Vote(proposalId, true) + voted := votes[voteKey] + if voted != true { + t.Errorf("Vote not recorded correctly") + } + + proposal := proposals[proposalId] + if proposal.Yea.Cmp(u256.NewUint(1_000_000)) != 0 { + t.Errorf("Vote count not updated correctly", proposal.Yea.ToString()) + } + + if proposal.Nay.Cmp(u256.NewUint(0)) != 0 { + t.Errorf("Vote count not updated correctly") + } + + if proposal.ExecutionState.Upcoming { + t.Errorf("Proposal should not be upcoming after vote") + } + + if !proposal.ExecutionState.Active { + t.Errorf("Proposal should be active after vote") + } + + proposalsJson := GetProposals() + if proposalsJson != `{"height":"156","now":"1234567956","proposals":[{"id":"1","configVersion":"1","proposer":"g1lmvrrrr4er2us84h2732sru76c9zl2nvknha8c","status":"eyJDcmVhdGVkQXQiOiIxMjM0NTY3OTM0IiwiVXBjb21pbmciOiJmYWxzZSIsIkFjdGl2ZSI6InRydWUiLCJWb3RpbmdTdGFydCI6IjEyMzQ1Njc5NDQiLCJWb3RpbmdFbmQiOiIxMjM0NTY3OTc0IiwiUGFzc2VkIjoiZmFsc2UiLCJQYXNzZWRBdCI6IjAiLCJSZWplY3RlZCI6ImZhbHNlIiwiUmVqZWN0ZWRBdCI6IjAiLCJDYW5jZWxlZCI6ImZhbHNlIiwiQ2FuY2VsZWRBdCI6IjAiLCJFeGVjdXRlZCI6ImZhbHNlIiwiRXhlY3V0ZWRBdCI6IjAiLCJFeHBpcmVkIjoiZmFsc2UiLCJFeHBpcmVkQXQiOiIwIn0=","type":"TEXT","title":"test_title","description":"test_description","vote":"eyJxdW9ydW0iOiI1MDAwMDAiLCJtYXgiOiIxMDAwMDAwIiwieWVzIjoiMTAwMDAwMCIsIm5vIjoiMCJ9","extra":""}]}` { + t.Errorf("Incorrect proposals json: %v", proposalsJson) + } + + votesJson := GetVotesByAddress(gsa.String()) + if votesJson != `{"height":"156","now":"1234567956","votes":[{"proposalId":"1","voteYes":"true","voteWeight":"1000000","voteHeight":"156","voteTimestamp":"1234567956"}]}` { + t.Errorf("Incorrect votes json: %v", votesJson) + } + + voteJson := GetVoteStatusFromProposalById(proposalId) + if voteJson != `{"height":"156","now":"1234567956","proposalId":"1","votes":"eyJxdW9ydW0iOiI1MDAwMDAiLCJtYXgiOiIxMDAwMDAwIiwieWVzIjoiMTAwMDAwMCIsIm5vIjoiMCJ9"}` { + t.Errorf("Incorrect vote json: %v", voteJson) + } + + addrVotesJson := GetVotesByAddress(gsa.String()) + if addrVotesJson != `{"height":"156","now":"1234567956","votes":[{"proposalId":"1","voteYes":"true","voteWeight":"1000000","voteHeight":"156","voteTimestamp":"1234567956"}]}` { + t.Errorf("Incorrect address votes json: %v", addrVotesJson) + } + + addrVoteJson := GetVoteByAddressFromProposalById(gsa.String(), proposalId) + if addrVoteJson != `{"height":"156","now":"1234567956","votes":[{"proposalId":"1","voteYes":"true","voteWeight":"1000000","voteHeight":"156","voteTimestamp":"1234567956"}]}` { + t.Errorf("Incorrect address vote json: %v", addrVoteJson) + } + }) + + t.Run("Vote twice", func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Errorf("The code did not panic") + } else if !strings.Contains(string(r), "already voted") { + t.Errorf("Unexpected panic message: %v", r) + } + }() + + Vote(proposalId, true) + }) + + t.Run("Vote after voting period", func(t *testing.T) { + std.TestSkipHeights(2001) + + defer func() { + if r := recover(); r == nil { + t.Errorf("The code did not panic") + } else if !strings.Contains(string(r), "has ended") { + t.Errorf("Unexpected panic message: %v", r) + } + }() + + Vote(proposalId, true) + }) + + t.Run("Proposal status after voting period", func(t *testing.T) { + updateProposalsState() + + proposal := proposals[proposalId] + if proposal.ExecutionState.Active { + t.Errorf("Proposal should not be active after voting period") + } + + if !proposal.ExecutionState.Passed { + t.Errorf("Proposal should be passed") + } + + if proposal.ExecutionState.Rejected { + t.Errorf("Proposal should not be rejected") + } + + if proposal.ExecutionState.Executed { + t.Errorf("Proposal should not be executed, #1 is text proposal and should not be executed") + } + }) +} + +func TestCancel(t *testing.T) { + // new text proposal #2 + proposalId := ProposeText("test2_title", "test2_description") + if proposalId != 2 { + t.Errorf("Expected proposal ID to be 2, got %d", proposalId) + } + + t.Run("Cancel non existent proposal", func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Errorf("The code did not panic") + } else if !strings.Contains(string(r), "does not exist") { + t.Errorf("Unexpected panic message: %v", r) + } + }() + + Cancel(uint64(123)) + }) + + t.Run("Successful cancel", func(t *testing.T) { + Cancel(proposalId) + proposalsJson := GetProposals() + if proposalsJson != `{"height":"2157","now":"1234571958","proposals":[{"id":"1","configVersion":"1","proposer":"g1lmvrrrr4er2us84h2732sru76c9zl2nvknha8c","status":"eyJDcmVhdGVkQXQiOiIxMjM0NTY3OTM0IiwiVXBjb21pbmciOiJmYWxzZSIsIkFjdGl2ZSI6ImZhbHNlIiwiVm90aW5nU3RhcnQiOiIxMjM0NTY3OTQ0IiwiVm90aW5nRW5kIjoiMTIzNDU2Nzk3NCIsIlBhc3NlZCI6InRydWUiLCJQYXNzZWRBdCI6IjEyMzQ1NzE5NTgiLCJSZWplY3RlZCI6ImZhbHNlIiwiUmVqZWN0ZWRBdCI6IjAiLCJDYW5jZWxlZCI6ImZhbHNlIiwiQ2FuY2VsZWRBdCI6IjAiLCJFeGVjdXRlZCI6ImZhbHNlIiwiRXhlY3V0ZWRBdCI6IjAiLCJFeHBpcmVkIjoiZmFsc2UiLCJFeHBpcmVkQXQiOiIwIn0=","type":"TEXT","title":"test_title","description":"test_description","vote":"eyJxdW9ydW0iOiI1MDAwMDAiLCJtYXgiOiIxMDAwMDAwIiwieWVzIjoiMTAwMDAwMCIsIm5vIjoiMCJ9","extra":""},{"id":"2","configVersion":"1","proposer":"g1lmvrrrr4er2us84h2732sru76c9zl2nvknha8c","status":"eyJDcmVhdGVkQXQiOiIxMjM0NTcxOTU4IiwiVXBjb21pbmciOiJmYWxzZSIsIkFjdGl2ZSI6ImZhbHNlIiwiVm90aW5nU3RhcnQiOiIxMjM0NTcxOTY4IiwiVm90aW5nRW5kIjoiMTIzNDU3MTk5OCIsIlBhc3NlZCI6ImZhbHNlIiwiUGFzc2VkQXQiOiIwIiwiUmVqZWN0ZWQiOiJmYWxzZSIsIlJlamVjdGVkQXQiOiIwIiwiQ2FuY2VsZWQiOiJ0cnVlIiwiQ2FuY2VsZWRBdCI6IjEyMzQ1NzE5NTgiLCJFeGVjdXRlZCI6ImZhbHNlIiwiRXhlY3V0ZWRBdCI6IjAiLCJFeHBpcmVkIjoiZmFsc2UiLCJFeHBpcmVkQXQiOiIwIn0=","type":"TEXT","title":"test2_title","description":"test2_description","vote":"eyJxdW9ydW0iOiI1MDAwMDAiLCJtYXgiOiIxMDAwMDAwIiwieWVzIjoiMCIsIm5vIjoiMCJ9","extra":""}]}` { + t.Errorf("Incorrect proposals json: %v", proposalsJson) + } + + proposalJson := GetProposalById(proposalId) + // vote quorum is 50% of total voting power + // which means xgns's balance of launchpad won't be affected + if proposalJson != `{"height":"2157","now":"1234571958","proposals":[{"id":"2","configVersion":"1","proposer":"g1lmvrrrr4er2us84h2732sru76c9zl2nvknha8c","status":"eyJDcmVhdGVkQXQiOiIxMjM0NTcxOTU4IiwiVXBjb21pbmciOiJmYWxzZSIsIkFjdGl2ZSI6ImZhbHNlIiwiVm90aW5nU3RhcnQiOiIxMjM0NTcxOTY4IiwiVm90aW5nRW5kIjoiMTIzNDU3MTk5OCIsIlBhc3NlZCI6ImZhbHNlIiwiUGFzc2VkQXQiOiIwIiwiUmVqZWN0ZWQiOiJmYWxzZSIsIlJlamVjdGVkQXQiOiIwIiwiQ2FuY2VsZWQiOiJ0cnVlIiwiQ2FuY2VsZWRBdCI6IjEyMzQ1NzE5NTgiLCJFeGVjdXRlZCI6ImZhbHNlIiwiRXhlY3V0ZWRBdCI6IjAiLCJFeHBpcmVkIjoiZmFsc2UiLCJFeHBpcmVkQXQiOiIwIn0=","type":"TEXT","title":"test2_title","description":"test2_description","vote":"eyJxdW9ydW0iOiI1MDAwMDAiLCJtYXgiOiIxMDAwMDAwIiwieWVzIjoiMCIsIm5vIjoiMCJ9","extra":""}]}` { + t.Errorf("Incorrect proposal json: %v", proposalJson) + } + + proposal := proposals[proposalId] + if !proposal.ExecutionState.Canceled { + t.Errorf("Proposal should be cancelled") + } + + if proposal.ExecutionState.Active { + t.Errorf("Proposal should not be active") + } + + if proposal.ExecutionState.Upcoming { + t.Errorf("Proposal should not be upcoming") + } + }) + + t.Run("Cancel already canceled proposal", func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Errorf("The code did not panic") + } else if !strings.Contains(string(r), "already canceled") { + t.Errorf("Unexpected panic message: %v", r) + } + }() + + Cancel(proposalId) + }) + + t.Run("Cancle after voting period", func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Errorf("The code did not panic") + } else if !strings.Contains(string(r), "already started") { + t.Errorf("Unexpected panic message: %v", r) + } + }() + + // new text proposal #3 + proposalId := ProposeText("test3_title", "test3_description") + std.TestSkipHeights(11) + Cancel(proposalId) + }) +} + +func TestExecute(t *testing.T) { + proposalId := uint64(1) // text proposal id + + t.Run("Execute text proposal", func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Errorf("The code did not panic") + } else if !strings.Contains(string(r), "not executable") { + t.Errorf("Unexpected panic message: %v", r) + } + }() + + Execute(proposalId) + }) +} + +func TestNoBehaviourProposal(t *testing.T) { + t.Run("Successful text proposal", func(t *testing.T) { + std.TestSetOrigCaller(gsa) + std.TestSetRealm(gsaRealm) + gns.Approve(a2u(consts.GOV_STAKER_ADDR), uint64(1_000_000)) + gs.Delegate(gsa, uint64(1_000_000)) + std.TestSkipHeights(11) // VotingWeightSmoothingDuration is 10 block + + proposalID := ProposeText("test_title", "test_description") + if proposalID != 4 { + t.Errorf("Expected proposal ID to be 4, got %d", proposalID) + } + + proposal, exist := proposals[proposalId] + if !exist { + t.Errorf("Proposal not found after creation") + } + + if proposal.Proposer != std.GetOrigCaller() { + t.Errorf("Incorrect proposer. Expected %v, got %v", std.GetOrigCaller(), proposal.Proposer) + } + + if proposal.ProposalType != "TEXT" { + t.Errorf("Incorrect proposal type. Expected TEXT, got %v", proposal.ProposalType) + } + + if !proposal.ExecutionState.Created { + t.Errorf("Proposal execute state(created) not set correctly") + } + + if !proposal.ExecutionState.Upcoming { + t.Errorf("Proposal execute state(upcoming) not set correctly") + } + + if proposal.Yea.Cmp(u256.NewUint(0)) != 0 || proposal.Nay.Cmp(u256.NewUint(0)) != 0 { + t.Errorf("Initial vote counts should be zero") + } + + if proposal.ConfigVersion != 1 { + t.Errorf("Initial config version should be 1") + } + + if proposal.Title != "test_title" { + t.Errorf("Incorrect title. Expected test_title, got %v", proposal.Title) + } + + if proposal.Description != "test_description" { + t.Errorf("Incorrect text. Expected test_description, got %v", proposal.Description) + } + + proposalsJson := GetProposalById(4) + if proposalsJson != `{"height":"2179","now":"1234572002","proposals":[{"id":"4","configVersion":"1","proposer":"g1lmvrrrr4er2us84h2732sru76c9zl2nvknha8c","status":"eyJDcmVhdGVkQXQiOiIxMjM0NTcyMDAyIiwiVXBjb21pbmciOiJ0cnVlIiwiQWN0aXZlIjoiZmFsc2UiLCJWb3RpbmdTdGFydCI6IjEyMzQ1NzIwMTIiLCJWb3RpbmdFbmQiOiIxMjM0NTcyMDQyIiwiUGFzc2VkIjoiZmFsc2UiLCJQYXNzZWRBdCI6IjAiLCJSZWplY3RlZCI6ImZhbHNlIiwiUmVqZWN0ZWRBdCI6IjAiLCJDYW5jZWxlZCI6ImZhbHNlIiwiQ2FuY2VsZWRBdCI6IjAiLCJFeGVjdXRlZCI6ImZhbHNlIiwiRXhlY3V0ZWRBdCI6IjAiLCJFeHBpcmVkIjoiZmFsc2UiLCJFeHBpcmVkQXQiOiIwIn0=","type":"TEXT","title":"test_title","description":"test_description","vote":"eyJxdW9ydW0iOiIxMDAwMDAwIiwibWF4IjoiMjAwMDAwMCIsInllcyI6IjAiLCJubyI6IjAifQ==","extra":""}]}` { + t.Errorf("Incorrect proposals json: %v", proposalsJson) + } + + // DO NOTHING + // JUST SKIP BUNCH OF TIME ( over voting period ) + std.TestSkipHeights(500) + + // CREATE NEW PROPOSAL + proposalID = ProposeText("test_title", "test_description") + if proposalID != 5 { + t.Errorf("Expected proposal ID to be 5, got %d", proposalID) + } + }) +} + +func TestMultipleProposalFromSameAddress(t *testing.T) { + t.Run("Successful two text proposal", func(t *testing.T) { + std.TestSetOrigCaller(gsa) + std.TestSetRealm(gsaRealm) + + proposalID := ProposeText("test_title", "test_description") + if proposalID != 6 { + t.Errorf("Expected proposal ID to be 6, got %d", proposalID) + } + + proposalID = ProposeText("test_title", "test_description") + if proposalID != 7 { + t.Errorf("Expected proposal ID to be 7, got %d", proposalID) + } + }) +} diff --git a/gov/staker/reward_calculation.gno b/gov/staker/reward_calculation.gno index f7f11d55..f1f2c16c 100644 --- a/gov/staker/reward_calculation.gno +++ b/gov/staker/reward_calculation.gno @@ -37,6 +37,32 @@ var ( userProtocolFeeReward = make(map[string]map[string]uint64) // address -> tokenPath -> tokenAmount ) +// === LAUNCHPAD DEPOSIT +var ( + // totalAmountByLaunchpad == xgns.BalanceOf(consts.LAUNCHPAD_ADDR) + amountByProjectWallet = make(map[string]uint64) // (project's) recipient wallet => amount + rewardByProjectWallet = make(map[string]uint64) // (project's) recipient wallet => reward +) + +func GetRewardByProjectWallet(addr std.Address) uint64 { + return rewardByProjectWallet[addr.String()] +} + +func SetAmountByProjectWallet(addr std.Address, amount uint64, add bool) { + prev := std.PrevRealm().PkgPath() + if prev != consts.LAUNCHPAD_PATH { + panic(ufmt.Sprintf("only launchpad can set amountByProjectWallet, called from %s", prev)) + } + + calculateReward() + + if add { + amountByProjectWallet[addr.String()] = amount + } else { + amountByProjectWallet[addr.String()] -= amount + } +} // LAUCNHAPD DEPOSIT === + func calculateReward() { println("[START START] calculateReward") height := uint64(std.GetHeight()) @@ -63,6 +89,20 @@ func calculateReward() { userXGnsRatio[delegator] = ratio } + // calculate project's recipient's xGNS ratio + // to calculate protocol fee + for recipient, amount := range amountByProjectWallet { + println("LAUNCHPAD_recipient:", recipient) + println("LAUNCHPAD_amount:", amount) + xGnsRecipientX96 := new(u256.Uint).Mul(u256.NewUint(amount), q96) + xGnsRecipientX96 = new(u256.Uint).Mul(xGnsRecipientX96, u256.NewUint(1_000_000_000)) + ratio := new(u256.Uint).Div(xGnsRecipientX96, xGnsX96) + ratio = ratio.Mul(ratio, q96) + ratio = ratio.Div(ratio, u256.NewUint(1_000_000_000)) + userXGnsRatio[recipient] = ratio + println("LAUNCHPAD_ratio:", ratio.ToString()) + } + calculateGNSEmission() calculateProtocolFee() @@ -172,7 +212,6 @@ func calculateProtocolFee() { println("FINAL LEFT TOKEN:", tokenPath, "AMOUNT:", leftProtocolFeeFromLast[tokenPath]) println() } - } func getGovStakerGnsWithoutXGns() uint64 { diff --git a/gov/staker/tests/__TEST_0_INIT_TOKEN_REGISTER_test.gno b/gov/staker/tests/__TEST_0_INIT_TOKEN_REGISTER_test.gno index df07020c..c32013e9 100644 --- a/gov/staker/tests/__TEST_0_INIT_TOKEN_REGISTER_test.gno +++ b/gov/staker/tests/__TEST_0_INIT_TOKEN_REGISTER_test.gno @@ -21,6 +21,7 @@ import ( pusers "gno.land/p/demo/users" + lp "gno.land/r/gnoswap/v2/launchpad" pl "gno.land/r/gnoswap/v2/pool" pf "gno.land/r/gnoswap/v2/protocol_fee" rr "gno.land/r/gnoswap/v2/router" @@ -164,6 +165,15 @@ func init() { pf.RegisterGRC20Interface("gno.land/r/onbloc/obl", OBLToken{}) pf.RegisterGRC20Interface("gno.land/r/gnoswap/v2/gns", GNSToken{}) + // LAUNCHPAD + lp.RegisterGRC20Interface("gno.land/r/onbloc/bar", BarToken{}) + lp.RegisterGRC20Interface("gno.land/r/onbloc/foo", FooToken{}) + lp.RegisterGRC20Interface("gno.land/r/onbloc/baz", BazToken{}) + lp.RegisterGRC20Interface("gno.land/r/onbloc/qux", QuxToken{}) + lp.RegisterGRC20Interface("gno.land/r/demo/wugnot", WugnotToken{}) + lp.RegisterGRC20Interface("gno.land/r/onbloc/obl", OBLToken{}) + lp.RegisterGRC20Interface("gno.land/r/gnoswap/v2/gns", GNSToken{}) + // GOV_STAKER RegisterGRC20Interface("gno.land/r/onbloc/bar", BarToken{}) RegisterGRC20Interface("gno.land/r/onbloc/foo", FooToken{}) diff --git a/gov/staker/tests/__TEST_api_test.gnoA b/gov/staker/tests/__TEST_api_test.gnoA index 57d81fc4..3a9e668c 100644 --- a/gov/staker/tests/__TEST_api_test.gnoA +++ b/gov/staker/tests/__TEST_api_test.gnoA @@ -44,6 +44,8 @@ func TestDelegate(t *testing.T) { gns.Approve(a2u(consts.GOV_STAKER_ADDR), uint64(1000001)) Delegate(dummyAddr, 1000001) + // from: admin + // to: dummyAddr shouldEQ(t, GetTotalStaked(), uint64(1000001)) shouldEQ(t, GetTotalDelegated(), uint64(1000001)) @@ -56,8 +58,8 @@ func TestDelegate(t *testing.T) { shouldEQ(t, GetVotingPowerBase(), `{"height":"123","now":"1234567890","totalDelegated":"1000001","votingPower":[{"address":"g1v36k6mteta047h6lta047h6lta047h6lz7gmv8","delegated":"1000001"}]}`) shouldEQ(t, GetVotingPowerBaseByAddress("g1v36k6mteta047h6lta047h6lta047h6lz7gmv8"), `{"height":"123","now":"1234567890","totalDelegated":"1000001","votingPower":[{"address":"g1v36k6mteta047h6lta047h6lta047h6lz7gmv8","delegated":"1000001"}]}`) - shouldEQ(t, GetDelegationHistoryByAddress(gsa.String()), ``) - shouldEQ(t, GetDelegationHistoryByAddress(dummyAddr.String()), `{"height":"123","now":"1234567890","delegationHistory":[{"to":"g1v36k6mteta047h6lta047h6lta047h6lz7gmv8","amount":"1000001","timestamp":"1234567890","add":"true"}]}`) + shouldEQ(t, GetDelegationHistoryByAddress(gsa.String()), `{"height":"123","now":"1234567890","delegationHistory":[{"to":"g1v36k6mteta047h6lta047h6lta047h6lz7gmv8","amount":"1000001","timestamp":"1234567890","add":"true"}]}`) + shouldEQ(t, GetDelegationHistoryByAddress(dummyAddr.String()), ``) } func TestRedelegateUnknownFrom(t *testing.T) { @@ -103,8 +105,9 @@ func TestRedelegate(t *testing.T) { shouldEQ(t, GetVotingPowerBase(), `{"height":"123","now":"1234567890","totalDelegated":"1000001","votingPower":[{"address":"g1v36k6mteta047h6lta047h6lta047h6lz7gmv8","delegated":"0"},{"address":"g1wfj5getvv4nkzar9ta047h6lta047h6lycyuqt","delegated":"1000001"}]}`) shouldEQ(t, GetVotingPowerBaseByAddress("g1v36k6mteta047h6lta047h6lta047h6lz7gmv8"), `{"height":"123","now":"1234567890","totalDelegated":"1000001","votingPower":[{"address":"g1v36k6mteta047h6lta047h6lta047h6lz7gmv8","delegated":"0"}]}`) shouldEQ(t, GetVotingPowerBaseByAddress("g1wfj5getvv4nkzar9ta047h6lta047h6lycyuqt"), `{"height":"123","now":"1234567890","totalDelegated":"1000001","votingPower":[{"address":"g1wfj5getvv4nkzar9ta047h6lta047h6lycyuqt","delegated":"1000001"}]}`) - shouldEQ(t, GetDelegationHistoryByAddress(gsa.String()), `{"height":"123","now":"1234567890","delegationHistory":[{"to":"g1v36k6mteta047h6lta047h6lta047h6lz7gmv8","amount":"1000001","timestamp":"1234567890","add":"false"}]}`) - shouldEQ(t, GetDelegationHistoryByAddress(dummyAddr.String()), `{"height":"123","now":"1234567890","delegationHistory":[{"to":"g1v36k6mteta047h6lta047h6lta047h6lz7gmv8","amount":"1000001","timestamp":"1234567890","add":"true"}]}`) + shouldEQ(t, GetDelegationHistoryByAddress(gsa.String()), `{"height":"123","now":"1234567890","delegationHistory":[{"to":"g1v36k6mteta047h6lta047h6lta047h6lz7gmv8","amount":"1000001","timestamp":"1234567890","add":"true"},{"to":"g1v36k6mteta047h6lta047h6lta047h6lz7gmv8","amount":"1000001","timestamp":"1234567890","add":"false"},{"to":"g1wfj5getvv4nkzar9ta047h6lta047h6lycyuqt","amount":"1000001","timestamp":"1234567890","add":"true"}]}`) + shouldEQ(t, GetDelegationHistoryByAddress(dummyAddr.String()), ``) + shouldEQ(t, GetDelegationHistoryByAddress(reDelegate.String()), ``) shouldEQ(t, GetLockedInfoByAddress(gsa.String()), ``) } @@ -142,7 +145,7 @@ func TestUndelegate(t *testing.T) { shouldEQ(t, GetVotingPowerBase(), `{"height":"123","now":"1234567890","totalDelegated":"0","votingPower":[{"address":"g1v36k6mteta047h6lta047h6lta047h6lz7gmv8","delegated":"0"},{"address":"g1wfj5getvv4nkzar9ta047h6lta047h6lycyuqt","delegated":"0"}]}`) shouldEQ(t, GetVotingPowerBaseByAddress("g1v36k6mteta047h6lta047h6lta047h6lz7gmv8"), `{"height":"123","now":"1234567890","totalDelegated":"0","votingPower":[{"address":"g1v36k6mteta047h6lta047h6lta047h6lz7gmv8","delegated":"0"}]}`) shouldEQ(t, GetVotingPowerBaseByAddress("g1wfj5getvv4nkzar9ta047h6lta047h6lycyuqt"), `{"height":"123","now":"1234567890","totalDelegated":"0","votingPower":[{"address":"g1wfj5getvv4nkzar9ta047h6lta047h6lycyuqt","delegated":"0"}]}`) - shouldEQ(t, GetDelegationHistoryByAddress(gsa.String()), `{"height":"123","now":"1234567890","delegationHistory":[{"to":"g1v36k6mteta047h6lta047h6lta047h6lz7gmv8","amount":"1000001","timestamp":"1234567890","add":"false"},{"to":"g1wfj5getvv4nkzar9ta047h6lta047h6lycyuqt","amount":"1000001","timestamp":"1234567890","add":"false"}]}`) + shouldEQ(t, GetDelegationHistoryByAddress(gsa.String()), `{"height":"123","now":"1234567890","delegationHistory":[{"to":"g1v36k6mteta047h6lta047h6lta047h6lz7gmv8","amount":"1000001","timestamp":"1234567890","add":"true"},{"to":"g1v36k6mteta047h6lta047h6lta047h6lz7gmv8","amount":"1000001","timestamp":"1234567890","add":"false"},{"to":"g1wfj5getvv4nkzar9ta047h6lta047h6lycyuqt","amount":"1000001","timestamp":"1234567890","add":"true"},{"to":"g1wfj5getvv4nkzar9ta047h6lta047h6lycyuqt","amount":"1000001","timestamp":"1234567890","add":"false"}]}`) shouldEQ(t, GetLockedInfoByAddress(gsa.String()), `{"height":"123","now":"1234567890","totalLocked":"1000001","claimableAmount":"0"}`) } @@ -161,7 +164,7 @@ func TestCollectAfter7Days(t *testing.T) { shouldEQ(t, GetVotingPowerBase(), `{"height":"123","now":"1234567890","totalDelegated":"0","votingPower":[{"address":"g1v36k6mteta047h6lta047h6lta047h6lz7gmv8","delegated":"0"},{"address":"g1wfj5getvv4nkzar9ta047h6lta047h6lycyuqt","delegated":"0"}]}`) shouldEQ(t, GetVotingPowerBaseByAddress("g1v36k6mteta047h6lta047h6lta047h6lz7gmv8"), `{"height":"123","now":"1234567890","totalDelegated":"0","votingPower":[{"address":"g1v36k6mteta047h6lta047h6lta047h6lz7gmv8","delegated":"0"}]}`) shouldEQ(t, GetVotingPowerBaseByAddress("g1wfj5getvv4nkzar9ta047h6lta047h6lycyuqt"), `{"height":"123","now":"1234567890","totalDelegated":"0","votingPower":[{"address":"g1wfj5getvv4nkzar9ta047h6lta047h6lycyuqt","delegated":"0"}]}`) - shouldEQ(t, GetDelegationHistoryByAddress(gsa.String()), `{"height":"123","now":"1234567890","delegationHistory":[{"to":"g1v36k6mteta047h6lta047h6lta047h6lz7gmv8","amount":"1000001","timestamp":"1234567890","add":"false"},{"to":"g1wfj5getvv4nkzar9ta047h6lta047h6lycyuqt","amount":"1000001","timestamp":"1234567890","add":"false"}]}`) + shouldEQ(t, GetDelegationHistoryByAddress(gsa.String()), `{"height":"123","now":"1234567890","delegationHistory":[{"to":"g1v36k6mteta047h6lta047h6lta047h6lz7gmv8","amount":"1000001","timestamp":"1234567890","add":"true"},{"to":"g1v36k6mteta047h6lta047h6lta047h6lz7gmv8","amount":"1000001","timestamp":"1234567890","add":"false"},{"to":"g1wfj5getvv4nkzar9ta047h6lta047h6lycyuqt","amount":"1000001","timestamp":"1234567890","add":"true"},{"to":"g1wfj5getvv4nkzar9ta047h6lta047h6lycyuqt","amount":"1000001","timestamp":"1234567890","add":"false"}]}`) shouldEQ(t, GetLockedAmount(), uint64(1000001)) shouldEQ(t, GetLockedInfoByAddress(gsa.String()), `{"height":"123","now":"1234567890","totalLocked":"1000001","claimableAmount":"0"}`) diff --git a/gov/staker/tests/__TEST_staker_04_protocol_fee_with_launchpad_test.gno b/gov/staker/tests/__TEST_staker_04_protocol_fee_with_launchpad_test.gno new file mode 100644 index 00000000..36021c2c --- /dev/null +++ b/gov/staker/tests/__TEST_staker_04_protocol_fee_with_launchpad_test.gno @@ -0,0 +1,208 @@ +package staker + +import ( + "std" + "testing" + "time" + + "gno.land/p/demo/testutils" + + "gno.land/r/gnoswap/v2/gns" + "gno.land/r/gnoswap/v2/gov/xgns" + + "gno.land/r/gnoswap/v2/consts" + en "gno.land/r/gnoswap/v2/emission" + lp "gno.land/r/gnoswap/v2/launchpad" + + // grc20 tokens + "gno.land/r/onbloc/bar" + "gno.land/r/onbloc/obl" + "gno.land/r/onbloc/qux" +) + +var ( + dummyToAddr = testutils.TestAddress("dummyTo") + dummyToRealm = std.NewUserRealm(dummyToAddr) + + dummyAddr = testutils.TestAddress("dummy") + dummyRealm = std.NewUserRealm(dummyAddr) + + reDelegate = testutils.TestAddress("reDelegate") + + // launchpad + projectAddr = testutils.TestAddress("projectAddr") + projectRealm = std.NewUserRealm(projectAddr) + + user01 = testutils.TestAddress("user01") + user01Realm = std.NewUserRealm(user01) +) + +func TestDelegateAdminToDummy(t *testing.T) { + std.TestSetOrigCaller(gsa) + std.TestSetRealm(gsaRealm) + + // gns before emission + shouldEQ(t, gns.BalanceOf(a2u(consts.STAKER_ADDR)), uint64(0)) + shouldEQ(t, gns.BalanceOf(a2u(consts.DEV_OPS)), uint64(0)) + shouldEQ(t, gns.BalanceOf(a2u(consts.COMMUNITY_POOL_ADDR)), uint64(0)) + shouldEQ(t, gns.BalanceOf(a2u(consts.GOV_STAKER_ADDR)), uint64(0)) + + std.TestSkipHeights(1) + gns.Approve(a2u(consts.GOV_STAKER_ADDR), uint64(1000001)) + Delegate(dummyToAddr, 1000001) + + // gns emission after adjustment & delegate + // 1 block gns 14269406 + // 75% ≈ 10702054.5 + // 20% ≈ 2853881.2 + // 5% ≈ 713470.3 + + shouldEQ(t, gns.BalanceOf(a2u(consts.STAKER_ADDR)), uint64(10702054)) + shouldEQ(t, gns.BalanceOf(a2u(consts.DEV_OPS)), uint64(2853881)) + shouldEQ(t, gns.BalanceOf(a2u(consts.COMMUNITY_POOL_ADDR)), uint64(713470)) + shouldEQ(t, gns.BalanceOf(a2u(consts.GOV_STAKER_ADDR)), uint64(1000001)) + shouldEQ(t, gns.BalanceOf(a2u(consts.GOV_STAKER_ADDR))-xgns.TotalSupply(), uint64(0)) +} + +func TestDelegateAnotherDumyToDummy_Self(t *testing.T) { + std.TestSetOrigCaller(gsa) + std.TestSetRealm(gsaRealm) + gns.Transfer(a2u(dummyAddr), 5000000) + + std.TestSkipHeights(1) + std.TestSetOrigCaller(dummyAddr) + std.TestSetRealm(dummyRealm) + gns.Approve(a2u(consts.GOV_STAKER_ADDR), uint64(5000000)) + Delegate(dummyAddr, 5000000) + + shouldEQ(t, gns.BalanceOf(a2u(consts.STAKER_ADDR)), uint64(21404109)) + shouldEQ(t, gns.BalanceOf(a2u(consts.DEV_OPS)), uint64(5707762)) + shouldEQ(t, gns.BalanceOf(a2u(consts.COMMUNITY_POOL_ADDR)), uint64(1426940)) + shouldEQ(t, gns.BalanceOf(a2u(consts.GOV_STAKER_ADDR)), uint64(6000001)) + shouldEQ(t, gns.BalanceOf(a2u(consts.GOV_STAKER_ADDR))-xgns.TotalSupply(), uint64(0)) +} + +func TestMockProtocolFee(t *testing.T) { + // admin > protocol_fee + // send qux, bar for testing + std.TestSetRealm(gsaRealm) + bar.Transfer(a2u(consts.PROTOCOL_FEE_ADDR), 1000) + qux.Transfer(a2u(consts.PROTOCOL_FEE_ADDR), 2500) + + shouldEQ(t, bar.BalanceOf(a2u(consts.PROTOCOL_FEE_ADDR)), uint64(1000)) + shouldEQ(t, bar.BalanceOf(a2u(consts.DEV_OPS)), uint64(0)) + shouldEQ(t, bar.BalanceOf(a2u(consts.GOV_STAKER_ADDR)), uint64(0)) + + shouldEQ(t, qux.BalanceOf(a2u(consts.PROTOCOL_FEE_ADDR)), uint64(2500)) + shouldEQ(t, qux.BalanceOf(a2u(consts.DEV_OPS)), uint64(0)) + shouldEQ(t, qux.BalanceOf(a2u(consts.GOV_STAKER_ADDR)), uint64(0)) +} + +func TestLaunchPadCreateProject(t *testing.T) { + std.TestSetRealm(gsaRealm) + std.TestSetOrigCaller(gsa) + + obl.Approve(a2u(consts.LAUNCHPAD_ADDR), 1_000_000_000) + projectId := lp.CreateProject( + oblPath, + projectAddr, + uint64(1_000_000_000), // 1000000000 + "", + "", + uint64(10), // 100000000 + uint64(20), // 200000000 + uint64(70), // 700000000 + uint64(time.Now().Unix()+10), // 5 block later + ) + shouldEQ(t, projectId, `gno.land/r/onbloc/obl:125`) + std.TestSkipHeights(10) // active project +} + +func TestLaunchPadDeposit(t *testing.T) { + std.TestSetRealm(gsaRealm) + std.TestSetOrigCaller(gsa) + gns.Transfer(a2u(user01), uint64(1_000_000)) // to deposit + + std.TestSetRealm(user01Realm) + std.TestSetOrigCaller(user01) + gns.Approve(a2u(consts.LAUNCHPAD_ADDR), uint64(1_000_000)) + + lp.DepositGns( + `gno.land/r/onbloc/obl:125:30`, + uint64(1_000_000), + ) + std.TestSkipHeights(1) +} + +func TestSkipDummyBlock(t *testing.T) { + std.TestSkipHeights(10) + en.MintAndDistributeGns() + calculateReward() // pf fee distribution being triggered + std.TestSkipHeights(1) + + shouldEQ(t, bar.BalanceOf(a2u(consts.PROTOCOL_FEE_ADDR)), uint64(0)) + shouldEQ(t, bar.BalanceOf(a2u(consts.DEV_OPS)), uint64(0)) + shouldEQ(t, bar.BalanceOf(a2u(consts.GOV_STAKER_ADDR)), uint64(1000)) + + shouldEQ(t, qux.BalanceOf(a2u(consts.PROTOCOL_FEE_ADDR)), uint64(0)) + shouldEQ(t, qux.BalanceOf(a2u(consts.DEV_OPS)), uint64(0)) + shouldEQ(t, qux.BalanceOf(a2u(consts.GOV_STAKER_ADDR)), uint64(2500)) +} + +func TestCheckReward(t *testing.T) { + t.Run("check dummy addr", func(t *testing.T) { + gcr := GetClaimableRewardByAddress(dummyAddr.String()) + shouldEQ(t, gcr, `{"height":"147","now":"1234567938","emissionReward":"0","protocolFees":[{"tokenPath":"gno.land/r/onbloc/bar","amount":"817"},{"tokenPath":"gno.land/r/onbloc/qux","amount":"2040"}]}`) + }) + + t.Run("check launchpad addr", func(t *testing.T) { + gcr := GetClaimableRewardByAddress(consts.LAUNCHPAD_ADDR.String()) + shouldEQ(t, gcr, `{"height":"147","now":"1234567938","emissionReward":"0"}`) + }) + + t.Run("check project's recipient", func(t *testing.T) { + gcr := GetClaimableRewardByAddress(projectAddr.String()) + shouldEQ(t, gcr, `{"height":"147","now":"1234567938","emissionReward":"0","protocolFees":[{"tokenPath":"gno.land/r/onbloc/bar","amount":"20"},{"tokenPath":"gno.land/r/onbloc/qux","amount":"51"}]}`) + }) +} + +func TestCollectReward(t *testing.T) { + std.TestSetOrigCaller(projectAddr) + std.TestSetRealm(projectRealm) + + // protocol fee has bar, qux + shouldEQ(t, bar.BalanceOf(a2u(projectAddr)), uint64(0)) + shouldEQ(t, qux.BalanceOf(a2u(projectAddr)), uint64(0)) + + CollectReward() + + shouldEQ(t, bar.BalanceOf(a2u(projectAddr)), uint64(20)) + shouldEQ(t, qux.BalanceOf(a2u(projectAddr)), uint64(51)) + +} + +func TestCollectRewardSameBlock(t *testing.T) { + std.TestSetOrigCaller(projectAddr) + std.TestSetRealm(projectRealm) + + shouldEQ(t, bar.BalanceOf(a2u(projectAddr)), uint64(20)) + shouldEQ(t, qux.BalanceOf(a2u(projectAddr)), uint64(51)) + + CollectReward() + + shouldEQ(t, bar.BalanceOf(a2u(projectAddr)), uint64(20)) + shouldEQ(t, qux.BalanceOf(a2u(projectAddr)), uint64(51)) +} + +func TestCollectRewardMoreBlock01(t *testing.T) { + std.TestSkipHeights(1) + std.TestSetOrigCaller(projectAddr) + std.TestSetRealm(projectRealm) + + shouldEQ(t, bar.BalanceOf(a2u(projectAddr)), uint64(20)) + shouldEQ(t, qux.BalanceOf(a2u(projectAddr)), uint64(51)) + + CollectReward() // even more block, protocol_fee didn't increase + shouldEQ(t, bar.BalanceOf(a2u(projectAddr)), uint64(20)) + shouldEQ(t, qux.BalanceOf(a2u(projectAddr)), uint64(51)) +} diff --git a/gov/xgns/xgns.gno b/gov/xgns/xgns.gno index 7996a15f..21750dc4 100644 --- a/gov/xgns/xgns.gno +++ b/gov/xgns/xgns.gno @@ -30,6 +30,15 @@ func init() { func TotalSupply() uint64 { return token.TotalSupply() } +func VotingSupply() uint64 { + total := token.TotalSupply() // this is entire amount of xGNS minted + + // this is amount of xGNS held by launchpad + // this xGNS doesn't participate in voting + launchpad := token.BalanceOf(consts.LAUNCHPAD_ADDR) + return total - launchpad +} + func BalanceOf(owner pusers.AddressOrName) uint64 { ownerAddr := users.Resolve(owner) return token.BalanceOf(ownerAddr) @@ -58,10 +67,10 @@ func Render(path string) string { func Mint(to pusers.AddressOrName, amount uint64) { common.IsHalted() - // only gov staker contract can call Mint + // only (gov staker) or (launchpad) contract can call Mint caller := std.PrevRealm().Addr() - if caller != consts.GOV_STAKER_ADDR { - panic("only gov staker contract can call Mint") + if caller != consts.GOV_STAKER_ADDR && caller != consts.LAUNCHPAD_ADDR { + panic("only (gov staker) or (launchpad) contract can call Mint") } checkErr(banker.Mint(users.Resolve(to), amount)) @@ -70,10 +79,10 @@ func Mint(to pusers.AddressOrName, amount uint64) { func Burn(from pusers.AddressOrName, amount uint64) { common.IsHalted() - // only gov staker contract can call Burn + // only (gov staker) or (launchpad) contract can call Mint caller := std.PrevRealm().Addr() - if caller != consts.GOV_STAKER_ADDR { - panic("only gov staker contract can call Burn") + if caller != consts.GOV_STAKER_ADDR && caller != consts.LAUNCHPAD_ADDR { + panic("only (gov staker) or (launchpad) contract can call Burn") } checkErr(banker.Burn(users.Resolve(from), amount)) diff --git a/launchpad/_RPC_api_deposit.gno b/launchpad/_RPC_api_deposit.gno new file mode 100644 index 00000000..cfa44af5 --- /dev/null +++ b/launchpad/_RPC_api_deposit.gno @@ -0,0 +1,35 @@ +package launchpad + +import ( + "std" +) + +func ApiGetClaimableDepositByAddress(address string) uint64 { + addr := std.Address(address) + if !addr.IsValid() { + return 0 + } + + gnsToUser := uint64(0) + for _, depositId := range depositsByUser[address] { + deposit := deposits[depositId] + + project, exist := projects[deposit.projectId] + if !exist { + continue + } + + tier := getTier(project, deposit.tier) + if checkTierActive(project, tier) { + continue + } + + if deposit.depositCollectHeight != 0 { + continue + } + + gnsToUser += deposit.amount // not reward amount, but deposit amount + } + + return gnsToUser +} diff --git a/launchpad/_RPC_api_project.gno b/launchpad/_RPC_api_project.gno new file mode 100644 index 00000000..ad163141 --- /dev/null +++ b/launchpad/_RPC_api_project.gno @@ -0,0 +1,43 @@ +package launchpad + +import ( + "std" + "time" + + "gno.land/p/demo/json" + "gno.land/p/demo/ufmt" +) + +func ApiGetProjectStatisticsByProjectId(projectId string) string { + project, exist := projects[projectId] + if !exist { + return "" + } + + totalDepositAmount := project.totalDepositAmount + actualDepositAmount := project.actualDepositAmount + + totalParticipant := project.totalParticipant + actualParticipant := project.actualParticipant + + totalCollectedAmount := project.totalCollectedAmount + + projectObj := metaNode() + projectObj.AppendObject("totalDepositAmount", json.StringNode("totalDepositAmount", ufmt.Sprintf("%d", totalDepositAmount))) + projectObj.AppendObject("actualDepositAmount", json.StringNode("actualDepositAmount", ufmt.Sprintf("%d", actualDepositAmount))) + projectObj.AppendObject("totalParticipant", json.StringNode("totalParticipant", ufmt.Sprintf("%d", totalParticipant))) + projectObj.AppendObject("actualParticipant", json.StringNode("actualParticipant", ufmt.Sprintf("%d", actualParticipant))) + projectObj.AppendObject("totalCollectedAmount", json.StringNode("totalCollectedAmount", ufmt.Sprintf("%d", totalCollectedAmount))) + + return marshal(projectObj) +} + +func metaNode() *json.Node { + height := std.GetHeight() + now := time.Now().Unix() + + metaObj := json.ObjectNode("", nil) + metaObj.AppendObject("height", json.StringNode("height", ufmt.Sprintf("%d", height))) + metaObj.AppendObject("now", json.StringNode("now", ufmt.Sprintf("%d", now))) + return metaObj +} diff --git a/launchpad/_RPC_api_reward.gno b/launchpad/_RPC_api_reward.gno new file mode 100644 index 00000000..1bf6f1bb --- /dev/null +++ b/launchpad/_RPC_api_reward.gno @@ -0,0 +1,62 @@ +package launchpad + +import ( + "std" + + gs "gno.land/r/gnoswap/v2/gov/staker" +) + +// protocol_fee reward for project's recipient +func ApiGetProjectRecipientRewardByProjectId(projectId string) string { + project, exist := projects[projectId] + if !exist { + return "0" + } + + return gs.GetClaimableRewardByAddress(project.recipient.String()) +} + +func ApiGetProjectRecipientRewardByAddress(address string) string { + addr := std.Address(address) + if !addr.IsValid() { + return "0" + } + + return gs.GetClaimableRewardByAddress(address) +} + +// project reward for deposit +func ApiGetDepositRewardByDepositId(depositId string) uint64 { + deposit, exist := deposits[depositId] + if !exist { + return 0 + } + + calculateDepositReward() + return deposit.rewardAmount +} + +func ApiGetDepositRewardByAddress(address string) uint64 { + addr := std.Address(address) + if !addr.IsValid() { + return 0 + } + + depositIds, exist := depositsByUser[address] + if !exist { + return 0 + } + + calculateDepositReward() + + totalReward := uint64(0) + for _, depositId := range depositIds { + deposit, exist := deposits[depositId] + if !exist { + continue + } + totalReward += deposit.rewardAmount + } + + return totalReward +} diff --git a/launchpad/consts.gno b/launchpad/consts.gno new file mode 100644 index 00000000..12ffc138 --- /dev/null +++ b/launchpad/consts.gno @@ -0,0 +1,19 @@ +package launchpad + +const ( + minimumGnsAmount = 1_000_000 +) + +// pool tier +const ( + TIMESTAMP_180DAYS = uint64(180 * 24 * 60 * 60) // 15552000 + TIMESTAMP_90DAYS = uint64(90 * 24 * 60 * 60) // 7776000 + TIMESTAMP_30DAYS = uint64(30 * 24 * 60 * 60) // 2592000 +) + +// claim wait duration for pool tier +const ( + TIMESTAMP_14DAYS = uint64(14 * 24 * 60 * 60) // 1209600, for 180 days + TIMESTAMP_7DAYS = uint64(7 * 24 * 60 * 60) // 604800, for 90 days + TIMESTAMP_3DAYS = uint64(3 * 24 * 60 * 60) // 259200, for 30 days +) diff --git a/launchpad/gno.mod b/launchpad/gno.mod new file mode 100644 index 00000000..081a7a5d --- /dev/null +++ b/launchpad/gno.mod @@ -0,0 +1 @@ +module gno.land/r/gnoswap/v2/launchpad diff --git a/launchpad/launchpad_deposit.gno b/launchpad/launchpad_deposit.gno new file mode 100644 index 00000000..12b4f831 --- /dev/null +++ b/launchpad/launchpad_deposit.gno @@ -0,0 +1,305 @@ +package launchpad + +import ( + "std" + "strings" + "time" + + "gno.land/p/demo/ufmt" + + "gno.land/r/gnoswap/v2/consts" + + "gno.land/r/gnoswap/v2/gns" + gs "gno.land/r/gnoswap/v2/gov/staker" + "gno.land/r/gnoswap/v2/gov/xgns" +) + +var ( + // depositId -> deposit + deposits = make(map[string]Deposit) + + // proejct -> tier -> []depositId + depositsByProject = make(map[string]map[string][]string) + + // user -> []depositId + depositsByUser = make(map[string][]string) + + // user -> project -> []depositId + depositsByUserByProject = make(map[string]map[string][]string) +) + +// DepositGns deposit gns to the project's tier +// - gns will be locked in `launchpad` contract +// - xgns will be minted to the `launchpad` contract +// +// returns depositId +func DepositGns( + targetProjectTierId string, + amount uint64, +) string { + projectId, tierStr := getProjectIdAndTierFromTierId(targetProjectTierId) + project, exist := projects[projectId] + if !exist { + panic(ufmt.Sprintf("project not found: %s", projectId)) + } + + // check conditions (grc20 tokens balance) + checkDepositConditions(project) + + // check if project is active + if !checkProjectActive(project) { + panic(ufmt.Sprintf("project is not active: %s", projectId)) + } + + // check if tier is active + tier := getTier(project, tierStr) + if !checkTierActive(project, tier) { + panic(ufmt.Sprintf("tier is not active: %s", tierStr)) + } + + // after all pre-checks + calculateDepositReward() + + // gns will be locked in `launchpad` contract + gns.TransferFrom( + a2u(std.GetOrigCaller()), + a2u(std.Address(consts.LAUNCHPAD_ADDR)), + amount, + ) + + // xgns will be minted to the `launchpad` contract + xgns.Mint( + a2u(std.Address(consts.LAUNCHPAD_ADDR)), + amount, + ) + + // update gov_staker contract's variable to calculate proejct's recipient's reward + gs.SetAmountByProjectWallet(project.recipient, amount, true) // true == add + + // update tier + tier.depositAmount += amount + tier.participant += 1 + project = setTier(project, tierStr, tier) + + // update project + project.totalDepositAmount += amount + project.actualDepositAmount += amount + project.totalParticipant += 1 + project.actualParticipant += 1 + projects[projectId] = project + + // deposit History + depositor := std.GetOrigCaller() + depositorStr := depositor.String() + depositId := projectId + ":" + tierStr + ":" + depositorStr + ":" + ufmt.Sprintf("%d", std.GetHeight()) + depositToHistory := Deposit{ + id: depositId, + projectId: projectId, + tier: tierStr, + depositor: depositor, + amount: amount, + depositHeight: uint64(std.GetHeight()), + depositTime: uint64(time.Now().Unix()), + } + + // update deposits + deposits[depositId] = depositToHistory + + // update depositsByUser + depositsByUser[depositorStr] = append(depositsByUser[depositorStr], depositId) + + // update depositsByProject + if _, exist := depositsByProject[projectId]; !exist { + depositsByProject[projectId] = make(map[string][]string) + } + if _, exist := depositsByProject[projectId][tierStr]; !exist { + depositsByProject[projectId][tierStr] = make([]string, 0) + } + depositsByProject[projectId][tierStr] = append(depositsByProject[projectId][tierStr], depositId) + + // update depositsByUserByProject + if _, exist := depositsByUserByProject[depositorStr]; !exist { + depositsByUserByProject[depositorStr] = make(map[string][]string) + } + if _, exist := depositsByUserByProject[depositorStr][projectId]; !exist { + depositsByUserByProject[depositorStr][projectId] = make([]string, 0) + } + depositsByUserByProject[depositorStr][projectId] = append(depositsByUserByProject[depositorStr][projectId], depositId) + + std.Emit( + "DepositGns", + "m_origCaller", origCaller(), + "m_prevRealm", prevRealm(), + "p_targetProjectTierId", targetProjectTierId, + "p_amount", ufmt.Sprintf("%d", amount), + "depositId", depositId, + ) + + return depositId +} + +// CollectDepositGns collect deposited gns +// - gns will be transfered from the `launchpad` to caller +// - launchpad's xgns will be burned +// +// returns collected gns amount +func CollectDepositGns() uint64 { + calculateDepositReward() // uncomment this line if L#208 `CollectReward` is removed + + caller := std.GetOrigCaller() + callerStr := caller.String() + userDeposits := depositsByUser[callerStr] + + gnsToUser := uint64(0) + for _, depositId := range userDeposits { + deposit := deposits[depositId] + + // check active + project, exist := projects[deposit.projectId] + if !exist { + panic(ufmt.Sprintf("SHOULD_NOT_HAPPEN__project not found: %s", deposit.projectId)) + } + + tier := getTier(project, deposit.tier) + if checkTierActive(project, tier) { + println("CollectDepositGns()_STILL ACTIVE TIER", deposit.tier) + continue + } + + // collected + if deposit.depositCollectHeight != 0 { + continue + } + + deposit.depositCollectHeight = uint64(std.GetHeight()) + deposit.depositCollectTime = uint64(time.Now().Unix()) + deposits[deposit.id] = deposit + + gnsToUser += deposit.amount + + // update gov_staker contract's variable to calculate proejct's recipient's reward + gs.SetAmountByProjectWallet(project.recipient, deposit.amount, false) // subtract + + // update tier + tier.depositAmount -= deposit.amount + tier.participant -= 1 + + // update project + project = setTier(project, deposit.tier, tier) + project.actualDepositAmount -= deposit.amount + project.actualParticipant -= 1 + projects[deposit.projectId] = project + + // emit event for each deposit + std.Emit( + "CollectDepositGns", + "m_origCaller", origCaller(), + "m_prevRealm", prevRealm(), + "depositId", depositId, + "amount", ufmt.Sprintf("%d", deposit.amount), + ) + } + + if gnsToUser > 0 { + xgns.Burn(a2u(consts.LAUNCHPAD_ADDR), gnsToUser) + gns.Transfer(a2u(caller), gnsToUser) + + // umcomment L#147 `calculateDepositReward()` + // CollectReward() + + return gnsToUser // return accumulated gns amount being withdrawn + } + + return 0 +} + +func getProjectIdFromTierId(tierId string) string { + // input: gno.land/r/gnoswap/gns:123:30 + // output: gno.land/r/gnoswap/gns:123 + + result := strings.Split(tierId, ":") + if len(result) == 3 { + return result[0] + ":" + result[1] + } + + panic(ufmt.Sprintf("invalid tierId: %s", tierId)) +} + +func getProjectIdAndTierFromTierId(tierId string) (string, string) { + result := strings.Split(tierId, ":") + if len(result) == 3 { + return result[0] + ":" + result[1], result[2] + } + + panic(ufmt.Sprintf("invalid tierId: %s", tierId)) +} + +func checkDepositConditions(project Project) { + if project.conditions == nil { + return + } + + for _, condition := range project.conditions { + if condition.minAmount == 0 { + continue + } else { + // check balance + balance := balanceOfByRegisterCall(condition.tokenPath, std.GetOrigCaller()) + if balance < condition.minAmount { + panic(ufmt.Sprintf("insufficient balance(%d) for token(%s)", balance, condition.tokenPath)) + } + } + } +} + +func checkProjectActive(project Project) bool { + if project.startHeight > uint64(std.GetHeight()) { + // not started yet + println(ufmt.Sprintf("checkProjectActive()__project not started yet // startHeight: %d // now: %d", project.startHeight, uint64(std.GetHeight()))) + return false + } + + if project.endHeight < uint64(std.GetHeight()) { + // already ended + println(ufmt.Sprintf("checkProjectActive()__project already ended // endHeight: %d // now: %d", project.endHeight, uint64(std.GetHeight()))) + return false + } + + return true +} + +func checkTierActive(project Project, tier Tier) bool { + if tier.endHeight < uint64(std.GetHeight()) { + return false + } + + return true +} + +func getTier(project Project, tierStr string) Tier { + switch tierStr { + case "30": + return project.tier30 + case "90": + return project.tier90 + case "180": + return project.tier180 + default: + panic(ufmt.Sprintf("getTier()__invalid tierStr: %s", tierStr)) + } +} + +func setTier(project Project, tierStr string, tier Tier) Project { + switch tierStr { + case "30": + project.tier30 = tier + case "90": + project.tier90 = tier + case "180": + project.tier180 = tier + default: + panic(ufmt.Sprintf("setTier()__invalid tierStr: %s", tierStr)) + } + + return project +} diff --git a/launchpad/launchpad_init.gno b/launchpad/launchpad_init.gno new file mode 100644 index 00000000..830d2f82 --- /dev/null +++ b/launchpad/launchpad_init.gno @@ -0,0 +1,281 @@ +package launchpad + +import ( + "std" + "strings" + "time" + + "gno.land/p/demo/ufmt" + u256 "gno.land/p/gnoswap/uint256" + + "gno.land/r/gnoswap/v2/consts" + + "gno.land/r/gnoswap/v2/gns" +) + +var ( + projects = make(map[string]Project) +) + +var ( + q96 = u256.Zero() +) + +func init() { + println("main_init") + q96 = u256.MustFromDecimal(consts.Q96) +} + +// CreateProject creates a new project +// - only admin can create project +// +// returns projectId +func CreateProject( + tokenPath string, + recipient std.Address, + depositAmount uint64, + conditionsToken string, // SEP BY *PAD* + conditionsAmount string, // SEP BY *PAD* + tier30Ratio uint64, // 10 => 10% + tier90Ratio uint64, // 20 + tier180Ratio uint64, // 70 + startTime uint64, +) string { // projectId + // only admin can create project + if std.GetOrigCaller() != consts.GNOSWAP_ADMIN { + panic("only admin can create project") + } + + if _, exist := registered[tokenPath]; !exist { + panic("token not registered") + } + + if !(recipient.IsValid()) { + panic("invalid recipient addr") + } + + if depositAmount == 0 { + panic("can not deposit 0") + } + + if strings.Contains(conditionsToken, "*PAD*") { + tokensToCheck := strings.Split(conditionsToken, "*PAD*") + for _, token := range tokensToCheck { + if _, exist := registered[token]; !exist { + panic(ufmt.Sprintf("condition token(%s) not registered", token)) + } + } + } + + projectId := generateProjectId(tokenPath) + _, exist := projects[projectId] + if exist { + panic("project already exist") + } + + if tier30Ratio+tier90Ratio+tier180Ratio != 100 { + panic("invalid ratio, sum of all tiers should be 100") + } + + if startTime <= uint64(time.Now().Unix()) { + panic("invalid start time, can not start project in past") + } + + avgBlockTimeMs := uint64(gns.GetAvgBlockTimeInMs()) + + height := uint64(std.GetHeight()) + now := uint64(time.Now().Unix()) + + timeUntilStart := startTime - now + blockDuration := timeUntilStart * 1000 / avgBlockTimeMs + startHeight := height + blockDuration + + transferFromByRegisterCall( + tokenPath, + std.GetOrigCaller(), + std.Address(consts.LAUNCHPAD_ADDR), + depositAmount, + ) + + tier30Amount := depositAmount * tier30Ratio / 100 + tier90Amount := depositAmount * tier90Ratio / 100 + tier180Amount := depositAmount * tier180Ratio / 100 + sumAll := tier30Amount + tier90Amount + tier180Amount + left := depositAmount - sumAll + if left > 0 { + // uAmounts can be left due to rounding + // XXX: how to handle this? + // tier180Amount += left + } + createdHeight := uint64(std.GetHeight()) + createdTime := uint64(time.Now().Unix()) + + // check grc20 required conditions + conditions := makeConditions(conditionsToken, conditionsAmount) + + // create tier + tier30EndHeight := startHeight + (TIMESTAMP_30DAYS * 1000 / avgBlockTimeMs) + tier30AmountX96 := new(u256.Uint).Mul(u256.NewUint(tier30Amount), q96) + tier30AmountPerBlockX96 := new(u256.Uint).Div(tier30AmountX96, u256.NewUint(tier30EndHeight-startHeight)) + tier30 := Tier{ + id: generateTierId(projectId, 30), + collectWaitDuration: TIMESTAMP_3DAYS * 1000 / avgBlockTimeMs, + tierAmount: tier30Amount, + tierAmountPerBlockX96: tier30AmountPerBlockX96, + endHeight: tier30EndHeight, + endTime: startTime + TIMESTAMP_30DAYS, + } + + tier90EndHeight := startHeight + (TIMESTAMP_90DAYS * 1000 / avgBlockTimeMs) + tier90AmountX96 := new(u256.Uint).Mul(u256.NewUint(tier90Amount), q96) + tier90AmountPerBlockX96 := new(u256.Uint).Div(tier90AmountX96, u256.NewUint(tier90EndHeight-startHeight)) + tier90 := Tier{ + id: generateTierId(projectId, 90), + collectWaitDuration: TIMESTAMP_7DAYS * 1000 / avgBlockTimeMs, + tierAmount: tier90Amount, + tierAmountPerBlockX96: tier90AmountPerBlockX96, + endHeight: tier90EndHeight, + endTime: startTime + TIMESTAMP_90DAYS, + } + + tier180EndHeight := startHeight + (TIMESTAMP_180DAYS * 1000 / avgBlockTimeMs) + tier180AmountX96 := new(u256.Uint).Mul(u256.NewUint(tier180Amount), q96) + tier180AmountPerBlockX96 := new(u256.Uint).Div(tier180AmountX96, u256.NewUint(tier180EndHeight-startHeight)) + tier180 := Tier{ + id: generateTierId(projectId, 180), + collectWaitDuration: TIMESTAMP_14DAYS * 1000 / avgBlockTimeMs, + tierAmount: tier180Amount, + tierAmountPerBlockX96: tier180AmountPerBlockX96, + endHeight: tier180EndHeight, + endTime: startTime + TIMESTAMP_180DAYS, + } + + // create project + project := Project{ + id: projectId, + tokenPath: tokenPath, + depositAmount: depositAmount, + recipient: recipient, + conditions: conditions, + tier30Ratio: tier30Ratio, + tier30: tier30, + tier90: tier90, + tier90Ratio: tier90Ratio, + tier180: tier180, + tier180Ratio: tier180Ratio, + createdHeight: createdHeight, + createdTime: createdTime, + startHeight: startHeight, + startTime: startTime, + endHeight: tier180.endHeight, + endTime: tier180.endTime, + } + + projects[projectId] = project + + std.Emit( + "CreateProject", + "m_origCaller", origCaller(), + "m_prevRealm", prevRealm(), + "p_tokenPath", tokenPath, + "p_recipient", recipient.String(), + "p_depositAmount", ufmt.Sprintf("%d", depositAmount), + "p_conditionsToken", conditionsToken, + "p_conditionsAmount", conditionsAmount, + "p_tier30Ratio", ufmt.Sprintf("%d", tier30Ratio), + "p_tier90Ratio", ufmt.Sprintf("%d", tier90Ratio), + "p_tier180Ratio", ufmt.Sprintf("%d", tier180Ratio), + "p_startHeight", ufmt.Sprintf("%d", startHeight), + "p_startTime", ufmt.Sprintf("%d", startTime), + "projectId", projectId, + + "tier30Amount", ufmt.Sprintf("%d", tier30Amount), + "tier30EndHeight", ufmt.Sprintf("%d", tier30EndHeight), + "tier30AmountPerBlockX96", tier30AmountPerBlockX96.ToString(), + + "tier90Amount", ufmt.Sprintf("%d", tier90Amount), + "tier90EndHeight", ufmt.Sprintf("%d", tier90EndHeight), + "tier90AmountPerBlockX96", tier90AmountPerBlockX96.ToString(), + + "tier180Amount", ufmt.Sprintf("%d", tier180Amount), + "tier180EndHeight", ufmt.Sprintf("%d", tier180EndHeight), + "tier180AmountPerBlockX96", tier180AmountPerBlockX96.ToString(), + ) + + return projectId +} + +// RefundProject refunds project's left token amount +// - only recipient can refund +// +// returns amount +func RefundProject(projectId string) uint64 { + caller := std.GetOrigCaller() + project, exist := projects[projectId] + if !exist { + panic("project not exist") + } + + recipient := project.recipient + if caller != recipient { + panic("only recipient can refund") + } + + endHeight := project.endHeight + height := uint64(std.GetHeight()) + + if endHeight > height { + panic("project not ended yet") + } + + toRefund := project.depositAmount - project.totalCollectedAmount + if toRefund > 0 { + transferByRegisterCall( + project.tokenPath, + recipient, + toRefund, + ) + + std.Emit( + "RefundProject", + "m_origCaller", origCaller(), + "m_prevRealm", prevRealm(), + "p_projectId", projectId, + "recipient", recipient.String(), + "toRefund", ufmt.Sprintf("%d", toRefund), + ) + } + + return toRefund +} + +func generateProjectId(tokenPath string) string { + // gno.land/r/gnoswap/gns:123 + return ufmt.Sprintf("%s:%d", tokenPath, std.GetHeight()) +} + +func generateTierId(projectId string, duration uint64) string { + // gno.land/r/gnoswap/gns:123:30 + return ufmt.Sprintf("%s:%d", projectId, duration) +} + +func makeConditions(conditionsToken string, conditionsAmount string) map[string]Condition { + if conditionsToken == "" || conditionsAmount == "" { + return nil + } + + conditions := make(map[string]Condition) + tokens := strings.Split(conditionsToken, "*PAD*") + amounts := strings.Split(conditionsAmount, "*PAD*") + if len(tokens) != len(amounts) { + panic(ufmt.Sprintf("invalid conditions(numTokens(%d) != numAmounts(%d))", len(tokens), len(amounts))) + } + + for i, token := range tokens { + conditions[token] = Condition{ + tokenPath: token, + minAmount: parseUint(amounts[i], 10, 64), + } + } + return conditions +} diff --git a/launchpad/launchpad_reward.gno b/launchpad/launchpad_reward.gno new file mode 100644 index 00000000..43fa54a3 --- /dev/null +++ b/launchpad/launchpad_reward.gno @@ -0,0 +1,532 @@ +package launchpad + +import ( + "std" + "time" + + "gno.land/p/demo/ufmt" + + u256 "gno.land/p/gnoswap/uint256" + + gs "gno.land/r/gnoswap/v2/gov/staker" + + "gno.land/r/gnoswap/v2/gns" +) + +// CollectProtocolFee collects protocol fee from gov/staker +// each project's recipient wallet will be rewarded +func CollectProtocolFee() { + gs.CollectReward() + // event will be emitted in gov/staker `CollectReward` +} + +var ( + lastCalculatedHeight uint64 +) + +func init() { + lastCalculatedHeight = uint64(std.GetHeight()) +} + +// CollectReward collects reward from entire deposit by caller +func CollectReward() { + calculateDepositReward() + + callerStr := std.GetOrigCaller().String() + depositIds, exist := depositsByUser[callerStr] + if !exist { + println("NO DEPOSIT FOR THIS USER", callerStr) + return + } + + // project token -> reward amount + toUser := make(map[string]uint64) + + avgBlockTimeMs := uint64(gns.GetAvgBlockTimeInMs()) + + for _, depositId := range depositIds { + deposit := deposits[depositId] + if deposit.rewardAmount == 0 { + println("NO REWARD FOR THIS DEPOSIT", depositId) + continue + } + + project := projects[deposit.projectId] + projectToken := project.tokenPath + + if deposit.rewardAmount > 0 { // deposit has some reward + if deposit.rewardCollectTime != 0 { // this collect is not first collect + println("(N)th collect") + toUser[projectToken] += deposit.rewardAmount + } else { + // if fisrt collect, then check tier's collect wait duration + collectableAfter := uint64(0) + switch deposit.tier { + case "30": + collectableAfter = project.startHeight + project.tier30.collectWaitDuration + case "90": + collectableAfter = project.startHeight + project.tier90.collectWaitDuration + case "180": + collectableAfter = project.startHeight + project.tier180.collectWaitDuration + } + + if uint64(std.GetHeight()) < collectableAfter { + println("NOT CLAIMABLE YET") + continue + } + + println("token:", projectToken, "reward:", deposit.rewardAmount) + toUser[projectToken] += deposit.rewardAmount + } + + std.Emit( + "CollectReward", + "m_origCaller", origCaller(), + "m_prevRealm", prevRealm(), + "depositId", depositId, + "amount", ufmt.Sprintf("%d", deposit.rewardAmount), + ) + } + + // update project + project.totalCollectedAmount += deposit.rewardAmount + projects[deposit.projectId] = project + + // update deposit + deposit.rewardAmount = 0 + deposit.rewardCollectHeight = uint64(std.GetHeight()) + deposit.rewardCollectTime = uint64(time.Now().Unix()) + deposits[depositId] = deposit + } + + // transfer reward to user + for tokenPath, amount := range toUser { + // println("tokenPath:", tokenPath) + // println("amount:", amount) + + if amount > 0 { + transferByRegisterCall(tokenPath, std.GetOrigCaller(), amount) + } + } +} + +// CollectRewardByProjectId collects reward from entire deposit of certain project by caller +// +// returns collected reward amount +func CollectRewardByProjectId(projectId string) uint64 { + project, exist := projects[projectId] + if !exist { + println("NO PROJECT FOR THIS ID", projectId) + return 0 + } + + calculateDepositReward() + + callerStr := std.GetOrigCaller().String() + if _, exist := depositsByUserByProject[callerStr]; !exist { + println("NO DEPOSIT FOR THIS USER", callerStr) + return 0 + } + depositIds, exist := depositsByUserByProject[callerStr][projectId] + if !exist { + println("NO DEPOSIT FOR THIS PROJECT", projectId) + return 0 + } + + toUser := uint64(0) + for _, depositId := range depositIds { + deposit := deposits[depositId] + if deposit.rewardAmount == 0 { + println("NO REWARD FOR THIS DEPOSIT", depositId) + continue + } + + project := projects[deposit.projectId] + if project.id != projectId { + println("PROJECT ID MISMATCH", project.id, projectId) + continue + } + + if deposit.rewardAmount > 0 { + if deposit.rewardCollectTime != 0 { + println("(N)th collect") + toUser += deposit.rewardAmount + } else { + collectableAfter := uint64(0) + switch deposit.tier { + case "30": + collectableAfter = project.startHeight + project.tier30.collectWaitDuration + case "90": + collectableAfter = project.startHeight + project.tier90.collectWaitDuration + case "180": + collectableAfter = project.startHeight + project.tier180.collectWaitDuration + } + + if uint64(std.GetHeight()) < collectableAfter { + println("NOT CLAIMABLE YET") + continue + } + + println("token:", project.tokenPath, "reward:", deposit.rewardAmount) + toUser += deposit.rewardAmount + } + + std.Emit( + "CollectRewardByProjectId", + "m_origCaller", origCaller(), + "m_prevRealm", prevRealm(), + "p_projectId", projectId, + "depositId", depositId, + "amount", ufmt.Sprintf("%d", deposit.rewardAmount), + ) + } + + // update project + project.totalCollectedAmount += deposit.rewardAmount + projects[deposit.projectId] = project + + // update deposit + deposit.rewardAmount = 0 + deposit.rewardCollectHeight = uint64(std.GetHeight()) + deposit.rewardCollectTime = uint64(time.Now().Unix()) + deposits[depositId] = deposit + } + + // transfer reward to user + transferByRegisterCall(project.tokenPath, std.GetOrigCaller(), toUser) + + return toUser +} + +// CollectRewardByProjectTier collects reward from entire deposit of certain project tier by caller +// +// returns collected reward amount +func CollectRewardByProjectTier(tierId string) uint64 { + projectId, tierStr := getProjectIdAndTierFromTierId(tierId) + project, exist := projects[projectId] + if !exist { + println("NO PROJECT FOR THIS ID", projectId) + return 0 + } + + callerStr := std.GetOrigCaller().String() + if _, exist := depositsByUserByProject[callerStr]; !exist { + println("NO DEPOSIT FOR THIS USER", callerStr) + return 0 + } + depositIds, exist := depositsByUserByProject[callerStr][projectId] + if !exist { + println("NO DEPOSIT FOR THIS PROJECT", projectId) + return 0 + } + + calculateDepositReward() + + toUser := uint64(0) + for _, depositId := range depositIds { + println("depositId:", depositId) + deposit := deposits[depositId] + + // matching tier + if deposit.projectId == projectId && deposit.tier == tierStr { + if deposit.rewardAmount == 0 { + println("NO REWARD FOR THIS DEPOSIT", depositId) + continue + } + + project := projects[deposit.projectId] + if project.id != projectId { + println("PROJECT ID MISMATCH", project.id, projectId) + continue + } + + if deposit.rewardAmount > 0 { + if deposit.rewardCollectTime != 0 { + println("(N)th collect") + toUser += deposit.rewardAmount + } else { + collectableAfter := uint64(0) + switch deposit.tier { + case "30": + collectableAfter = project.startHeight + project.tier30.collectWaitDuration + case "90": + collectableAfter = project.startHeight + project.tier90.collectWaitDuration + case "180": + collectableAfter = project.startHeight + project.tier180.collectWaitDuration + } + + if uint64(std.GetHeight()) < collectableAfter { + println("NOT CLAIMABLE YET") + continue + } + + println("token:", project.tokenPath, "reward:", deposit.rewardAmount) + toUser += deposit.rewardAmount + } + } + + std.Emit( + "CollectRewardByProjectTier", + "m_origCaller", origCaller(), + "m_prevRealm", prevRealm(), + "p_tierId", tierId, + "depositId", depositId, + "amount", ufmt.Sprintf("%d", deposit.rewardAmount), + ) + } + + // update project + project.totalCollectedAmount += deposit.rewardAmount + projects[deposit.projectId] = project + + // update deposit + deposit.rewardAmount = 0 + deposit.rewardCollectHeight = uint64(std.GetHeight()) + deposit.rewardCollectTime = uint64(time.Now().Unix()) + deposits[depositId] = deposit + } + + // transfer reward to user + transferByRegisterCall(project.tokenPath, std.GetOrigCaller(), toUser) + + // XXX: emit event + + return toUser +} + +// CollectRewardByDepositId collects reward from certain deposit by caller +// +// returns collected reward amount +func CollectRewardByDepositId(depositId string) uint64 { + println("CollectRewardByDepositId", depositId) + deposit, exist := deposits[depositId] + if !exist { + panic("deposit not found") + } + + project, exist := projects[deposit.projectId] + if !exist { + println("NO PROJECT FOR THIS ID", deposit.projectId) + return 0 + } + + callerStr := std.GetOrigCaller().String() + if _, exist := depositsByUserByProject[callerStr]; !exist { + println("NO DEPOSIT FOR THIS USER", callerStr) + return 0 + } + + calculateDepositReward() + deposit = deposits[depositId] // get updated deposit + + toUser := uint64(0) + + if deposit.rewardAmount > 0 { + if deposit.rewardCollectTime != 0 { + println("(N)th collect") + toUser += deposit.rewardAmount + } else { + collectableAfter := uint64(0) + switch deposit.tier { + case "30": + collectableAfter = project.startHeight + project.tier30.collectWaitDuration + case "90": + collectableAfter = project.startHeight + project.tier90.collectWaitDuration + case "180": + collectableAfter = project.startHeight + project.tier180.collectWaitDuration + } + + if uint64(std.GetHeight()) < collectableAfter { + println("NOT CLAIMABLE YET") + return 0 + } + + toUser += deposit.rewardAmount + } + + std.Emit( + "CollectRewardByDepositId", + "m_origCaller", origCaller(), + "m_prevRealm", prevRealm(), + "p_depositId", depositId, + "amount", ufmt.Sprintf("%d", deposit.rewardAmount), + ) + + // update project + project.totalCollectedAmount += deposit.rewardAmount + projects[deposit.projectId] = project + + // update deposit + deposit.rewardAmount = 0 + deposit.rewardCollectHeight = uint64(std.GetHeight()) + deposit.rewardCollectTime = uint64(time.Now().Unix()) + deposits[depositId] = deposit + } + + // transfer reward to user + transferByRegisterCall(project.tokenPath, std.GetOrigCaller(), toUser) + + // XXX: emit event + + return toUser +} + +var lastCalculateHeightForProjectTier = make(map[string]uint64) // using height + +// amount of project token for each deposit will be calculated +func calculateDepositReward() { + // println(">>> calculateDepositReward") + height := uint64(std.GetHeight()) + now := uint64(time.Now().Unix()) + + if height == lastCalculatedHeight { + println("THIS BLOCK ALREADY CALCULATED", height) + return + } + lastCalculatedHeight = height + + for projectIdx, project := range projects { + // // loop with project + // println("projectIdx", projectIdx) + // println("project.id", project.id) + + // // early return if not active + // println("project.startHeight\t", project.startHeight) + // println("now\t\t\t", now) + if project.startHeight > height { + println("PROJECT NOT STARTED") + continue + } + + // println("LAST CALC PROJECT TIER 30", project.tier30.id) + // println("lastCalculateHeightForProjectTier[project.tier30.id]", lastCalculateHeightForProjectTier[project.tier30.id]) + if lastCalculateHeightForProjectTier[project.tier30.id] == 0 { + // println(" > INIT TO `project.startHeight`", project.startHeight) + lastCalculateHeightForProjectTier[project.tier30.id] = project.startHeight + } + + // println("LAST CALC PROJECT TIER 90", project.tier90.id) + // println("lastCalculateHeightForProjectTier[project.tier90.id]", lastCalculateHeightForProjectTier[project.tier90.id]) + if lastCalculateHeightForProjectTier[project.tier90.id] == 0 { + // println(" > INIT TO `project.startHeight`", project.startHeight) + lastCalculateHeightForProjectTier[project.tier90.id] = project.startHeight + } + + // println("LAST CALC PROJECT TIER 180", project.tier180.id) + // println("lastCalculateHeightForProjectTier[project.tier180.id]", lastCalculateHeightForProjectTier[project.tier180.id]) + if lastCalculateHeightForProjectTier[project.tier180.id] == 0 { + // println(" > INIT TO `project.startHeight`", project.startHeight) + lastCalculateHeightForProjectTier[project.tier180.id] = project.startHeight + } + + // if current height is greater than endHeight, then use endHeight as project's each tier's endHeight + endHeightFor30 := minU64(height, project.tier30.endHeight) + endHeightFor90 := minU64(height, project.tier90.endHeight) + endHeightFor180 := minU64(height, project.tier180.endHeight) + + // if last calculate height is greater than endHeight, then use endHeight to calculate duration + sinceLast30 := endHeightFor30 - minU64(endHeightFor30, lastCalculateHeightForProjectTier[project.tier30.id]) + sinceLast90 := endHeightFor90 - minU64(endHeightFor90, lastCalculateHeightForProjectTier[project.tier90.id]) + sinceLast180 := endHeightFor180 - minU64(endHeightFor180, lastCalculateHeightForProjectTier[project.tier180.id]) + + // update for next calc + lastCalculateHeightForProjectTier[project.tier30.id] = height + lastCalculateHeightForProjectTier[project.tier90.id] = height + lastCalculateHeightForProjectTier[project.tier180.id] = height + + rewardX96_30 := new(u256.Uint).Mul(project.tier30.tierAmountPerBlockX96, u256.NewUint(sinceLast30)) + rewardX96_90 := new(u256.Uint).Mul(project.tier90.tierAmountPerBlockX96, u256.NewUint(sinceLast90)) + rewardX96_180 := new(u256.Uint).Mul(project.tier180.tierAmountPerBlockX96, u256.NewUint(sinceLast180)) + + // calculate deposit ratio + // loop with each tier (30 90 180) + tier30Deposit := project.tier30.depositAmount + tier90Deposit := project.tier90.depositAmount + tier180Deposit := project.tier180.depositAmount + // println("tier30.id", project.tier30.id) + // println("tier30Deposit", tier30Deposit) + // println("tier90Deposit", tier90Deposit) + // println("tier180Deposit", tier180Deposit) + + // iterate deposit (by project) + depositWithTier, exist := depositsByProject[project.id] + if !exist { + println("NO DEPOSIT FOR THIS PROJECT", project.id) + return + } + + for tierStr, depositIds := range depositWithTier { + println("tierStr", tierStr) + tierAmount := uint64(0) + var rewardX96 *u256.Uint + + switch tierStr { + case "30": + tierAmount = tier30Deposit + case "90": + tierAmount = tier90Deposit + case "180": + tierAmount = tier180Deposit + } + + for _, depositId := range depositIds { + deposit := deposits[depositId] + + sinceLast := uint64(0) + switch deposit.tier { + case "30": + sinceLast = sinceLast30 + rewardX96 = rewardX96_30.Clone() + case "90": + sinceLast = sinceLast90 + rewardX96 = rewardX96_90.Clone() + case "180": + sinceLast = sinceLast180 + rewardX96 = rewardX96_180.Clone() + default: + panic("INVALID TIER") + } + if sinceLast == 0 { + println("NO BLOCK PASSED SINCE LAST CALCULATION") + continue + } + + // calculate reward + ratioX96 := calcDepositRatioX96(tierAmount, deposit.amount) + println("ratioX96:", ratioX96.ToString()) + + depositRewardX96X96 := u256.Zero().Mul(rewardX96, ratioX96) + println("depositRewardX96X96:", depositRewardX96X96.ToString()) + + depositRewardX96 := u256.Zero().Div(depositRewardX96X96, q96) + println("depositRewardX96:", depositRewardX96.ToString()) + + depositRewardX := u256.Zero().Div(depositRewardX96, q96) + println("depositRewardX:", depositRewardX.ToString()) + + depoistReward := depositRewardX.Uint64() + println("depoistReward:", depoistReward) + + println("B_deposit.rewardAmount", deposit.rewardAmount) + deposit.rewardAmount += depoistReward + println("A_deposit.rewardAmount", deposit.rewardAmount) + + // update deposit + deposits[depositId] = deposit + } + } + } +} + +func calcDepositRatioX96(tierAmount uint64, amount uint64) *u256.Uint { + amountX96 := new(u256.Uint).Mul(u256.NewUint(amount), q96) + amountX96x := new(u256.Uint).Mul(amountX96, u256.NewUint(1_000_000_000)) + + tierAmountX96 := new(u256.Uint).Mul(u256.NewUint(tierAmount), q96) + + depositRatioX96 := new(u256.Uint).Div(amountX96x, tierAmountX96) + depositRatioX96 = depositRatioX96.Mul(depositRatioX96, q96) + depositRatioX96 = depositRatioX96.Div(depositRatioX96, u256.NewUint(1_000_000_000)) + + return depositRatioX96 +} diff --git a/launchpad/tests/__TEST_0_INIT_TOKEN_REGISTER_test.gno b/launchpad/tests/__TEST_0_INIT_TOKEN_REGISTER_test.gno new file mode 100644 index 00000000..03caf67c --- /dev/null +++ b/launchpad/tests/__TEST_0_INIT_TOKEN_REGISTER_test.gno @@ -0,0 +1,164 @@ +package launchpad + +import ( + "std" + + "gno.land/r/onbloc/foo" + + "gno.land/r/onbloc/bar" + + "gno.land/r/onbloc/baz" + + "gno.land/r/onbloc/qux" + + "gno.land/r/demo/wugnot" + + "gno.land/r/onbloc/obl" + + "gno.land/r/gnoswap/v2/gns" + + "gno.land/r/gnoswap/v2/consts" + + pusers "gno.land/p/demo/users" + + gs "gno.land/r/gnoswap/v2/gov/staker" + pf "gno.land/r/gnoswap/v2/protocol_fee" +) + +type FooToken struct{} + +func (FooToken) Transfer() func(to pusers.AddressOrName, amount uint64) { + return foo.Transfer +} +func (FooToken) TransferFrom() func(from, to pusers.AddressOrName, amount uint64) { + return foo.TransferFrom +} +func (FooToken) BalanceOf() func(owner pusers.AddressOrName) uint64 { + return foo.BalanceOf +} +func (FooToken) Approve() func(spender pusers.AddressOrName, amount uint64) { + return foo.Approve +} + +type BarToken struct{} + +func (BarToken) Transfer() func(to pusers.AddressOrName, amount uint64) { + return bar.Transfer +} +func (BarToken) TransferFrom() func(from, to pusers.AddressOrName, amount uint64) { + return bar.TransferFrom +} +func (BarToken) BalanceOf() func(owner pusers.AddressOrName) uint64 { + return bar.BalanceOf +} +func (BarToken) Approve() func(spender pusers.AddressOrName, amount uint64) { + return bar.Approve +} + +type BazToken struct{} + +func (BazToken) Transfer() func(to pusers.AddressOrName, amount uint64) { + return baz.Transfer +} +func (BazToken) TransferFrom() func(from, to pusers.AddressOrName, amount uint64) { + return baz.TransferFrom +} +func (BazToken) BalanceOf() func(owner pusers.AddressOrName) uint64 { + return baz.BalanceOf +} +func (BazToken) Approve() func(spender pusers.AddressOrName, amount uint64) { + return baz.Approve +} + +type QuxToken struct{} + +func (QuxToken) Transfer() func(to pusers.AddressOrName, amount uint64) { + return qux.Transfer +} +func (QuxToken) TransferFrom() func(from, to pusers.AddressOrName, amount uint64) { + return qux.TransferFrom +} +func (QuxToken) BalanceOf() func(owner pusers.AddressOrName) uint64 { + return qux.BalanceOf +} +func (QuxToken) Approve() func(spender pusers.AddressOrName, amount uint64) { + return qux.Approve +} + +type WugnotToken struct{} + +func (WugnotToken) Transfer() func(to pusers.AddressOrName, amount uint64) { + return wugnot.Transfer +} +func (WugnotToken) TransferFrom() func(from, to pusers.AddressOrName, amount uint64) { + return wugnot.TransferFrom +} +func (WugnotToken) BalanceOf() func(owner pusers.AddressOrName) uint64 { + return wugnot.BalanceOf +} +func (WugnotToken) Approve() func(spender pusers.AddressOrName, amount uint64) { + return wugnot.Approve +} + +type OBLToken struct{} + +func (OBLToken) Transfer() func(to pusers.AddressOrName, amount uint64) { + return obl.Transfer +} +func (OBLToken) TransferFrom() func(from, to pusers.AddressOrName, amount uint64) { + return obl.TransferFrom +} +func (OBLToken) BalanceOf() func(owner pusers.AddressOrName) uint64 { + return obl.BalanceOf +} +func (OBLToken) Approve() func(spender pusers.AddressOrName, amount uint64) { + return obl.Approve +} + +type GNSToken struct{} + +func (GNSToken) Transfer() func(to pusers.AddressOrName, amount uint64) { + return gns.Transfer +} + +func (GNSToken) TransferFrom() func(from, to pusers.AddressOrName, amount uint64) { + return gns.TransferFrom +} + +func (GNSToken) BalanceOf() func(owner pusers.AddressOrName) uint64 { + return gns.BalanceOf +} + +func (GNSToken) Approve() func(spender pusers.AddressOrName, amount uint64) { + return gns.Approve +} + +func init() { + std.TestSetOrigCaller(consts.TOKEN_REGISTER) + + // protocol_fee + pf.RegisterGRC20Interface("gno.land/r/onbloc/bar", BarToken{}) + pf.RegisterGRC20Interface("gno.land/r/onbloc/foo", FooToken{}) + pf.RegisterGRC20Interface("gno.land/r/onbloc/baz", BazToken{}) + pf.RegisterGRC20Interface("gno.land/r/onbloc/qux", QuxToken{}) + pf.RegisterGRC20Interface("gno.land/r/demo/wugnot", WugnotToken{}) + pf.RegisterGRC20Interface("gno.land/r/onbloc/obl", OBLToken{}) + pf.RegisterGRC20Interface("gno.land/r/gnoswap/v2/gns", GNSToken{}) + + // gov_staker + gs.RegisterGRC20Interface("gno.land/r/onbloc/bar", BarToken{}) + gs.RegisterGRC20Interface("gno.land/r/onbloc/foo", FooToken{}) + gs.RegisterGRC20Interface("gno.land/r/onbloc/baz", BazToken{}) + gs.RegisterGRC20Interface("gno.land/r/onbloc/qux", QuxToken{}) + gs.RegisterGRC20Interface("gno.land/r/demo/wugnot", WugnotToken{}) + gs.RegisterGRC20Interface("gno.land/r/onbloc/obl", OBLToken{}) + gs.RegisterGRC20Interface("gno.land/r/gnoswap/v2/gns", GNSToken{}) + + RegisterGRC20Interface("gno.land/r/onbloc/bar", BarToken{}) + RegisterGRC20Interface("gno.land/r/onbloc/foo", FooToken{}) + RegisterGRC20Interface("gno.land/r/onbloc/baz", BazToken{}) + RegisterGRC20Interface("gno.land/r/onbloc/qux", QuxToken{}) + RegisterGRC20Interface("gno.land/r/demo/wugnot", WugnotToken{}) + RegisterGRC20Interface("gno.land/r/onbloc/obl", OBLToken{}) + RegisterGRC20Interface("gno.land/r/gnoswap/v2/gns", GNSToken{}) +} diff --git a/launchpad/tests/__TEST_0_INIT_VARIABLE_AND_HELPER_test.gno b/launchpad/tests/__TEST_0_INIT_VARIABLE_AND_HELPER_test.gno new file mode 100644 index 00000000..e8c10f1b --- /dev/null +++ b/launchpad/tests/__TEST_0_INIT_VARIABLE_AND_HELPER_test.gno @@ -0,0 +1,80 @@ +package launchpad + +import ( + "std" + "testing" + + "gno.land/r/gnoswap/v2/consts" +) + +var ( + gsa std.Address = consts.GNOSWAP_ADMIN + + fooPath string = "gno.land/r/onbloc/foo" + barPath string = "gno.land/r/onbloc/bar" + bazPath string = "gno.land/r/onbloc/baz" + quxPath string = "gno.land/r/onbloc/qux" + + oblPath string = "gno.land/r/onbloc/obl" + // wugnotPath string = "gno.land/r/demo/wugnot" // from consts + // gnsPath string = "gno.land/r/gnoswap/v2/gns" // from consts + + fee100 uint32 = 100 + fee500 uint32 = 500 + fee3000 uint32 = 3000 + + maxApprove uint64 = 18446744073709551615 +) + +// Realms to mock frames +var ( + gsaRealm = std.NewUserRealm(gsa) + posRealm = std.NewCodeRealm(consts.POSITION_PATH) + rouRealm = std.NewCodeRealm(consts.ROUTER_PATH) +) + +/* HELPER */ +func shouldEQ(t *testing.T, got, expected interface{}) { + if got != expected { + t.Errorf("got\n%v\n\nexpected\n%v\n", 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() +} + +func ugnotBalanceOf(addr std.Address) uint64 { + testBanker := std.GetBanker(std.BankerTypeRealmIssue) + + coins := testBanker.GetCoins(addr) + if len(coins) == 0 { + return 0 + } + + return uint64(coins.AmountOf("ugnot")) +} diff --git a/launchpad/tests/__TEST_RPC_api_test.gnoA b/launchpad/tests/__TEST_RPC_api_test.gnoA new file mode 100644 index 00000000..62249a57 --- /dev/null +++ b/launchpad/tests/__TEST_RPC_api_test.gnoA @@ -0,0 +1,208 @@ +package launchpad + +import ( + "std" + "testing" + "time" + + "gno.land/p/demo/testutils" + + "gno.land/r/gnoswap/v2/consts" + + "gno.land/r/onbloc/bar" + "gno.land/r/onbloc/foo" + "gno.land/r/onbloc/obl" + "gno.land/r/onbloc/qux" + + "gno.land/r/gnoswap/v2/gns" +) + +var ( + projectAddr = testutils.TestAddress("projectAddr") + projectAddr02 = testutils.TestAddress("projectAddr02") + user01 = testutils.TestAddress("user01") + + projectRealm = std.NewUserRealm(projectAddr) + projectRealm02 = std.NewUserRealm(projectAddr02) + user01Realm = std.NewUserRealm(user01) +) + +func init() { + println("test_init") +} + +func TestMockProtocolFee(t *testing.T) { + // admin > protocol_fee + // send qux, bar for testing + std.TestSetRealm(gsaRealm) + bar.Transfer(a2u(consts.PROTOCOL_FEE_ADDR), 1000) + qux.Transfer(a2u(consts.PROTOCOL_FEE_ADDR), 2500) + + shouldEQ(t, bar.BalanceOf(a2u(consts.PROTOCOL_FEE_ADDR)), uint64(1000)) + shouldEQ(t, bar.BalanceOf(a2u(consts.DEV_OPS)), uint64(0)) + shouldEQ(t, bar.BalanceOf(a2u(consts.GOV_STAKER_ADDR)), uint64(0)) + + shouldEQ(t, qux.BalanceOf(a2u(consts.PROTOCOL_FEE_ADDR)), uint64(2500)) + shouldEQ(t, qux.BalanceOf(a2u(consts.DEV_OPS)), uint64(0)) + shouldEQ(t, qux.BalanceOf(a2u(consts.GOV_STAKER_ADDR)), uint64(0)) +} + +func TestCreateProject(t *testing.T) { + t.Run("recipient protocol_fee reward before creating project", func(t *testing.T) { + // check before project create + got := ApiGetProjectRecipientRewardByAddress(projectAddr.String()) + shouldEQ(t, got, `{"height":"123","now":"1234567890","emissionReward":"0"}`) + }) + + t.Run("create project", func(t *testing.T) { + std.TestSetRealm(gsaRealm) + std.TestSetOrigCaller(gsa) + obl.Approve(a2u(consts.LAUNCHPAD_ADDR), uint64(1_000_000_000)) + std.TestSkipHeights(1) + + projectId := CreateProject( + oblPath, + projectAddr, + uint64(1_000_000_000), // 1000000000 + "gno.land/r/onbloc/foo*PAD*gno.land/r/onbloc/bar", + "1*PAD*2", + uint64(10), // 100000000 + uint64(20), // 200000000 + uint64(70), // 700000000 + uint64(time.Now().Unix()+10), // 10s later + ) + shouldEQ(t, projectId, `gno.land/r/onbloc/obl:124`) + std.TestSkipHeights(1) + }) + + t.Run("recipient protocol_fee reward after creating project", func(t *testing.T) { + // check after project create + got := ApiGetProjectRecipientRewardByAddress(projectAddr.String()) + shouldEQ(t, got, `{"height":"125","now":"1234567894","emissionReward":"0"}`) + }) +} + +func TestCreateProject02(t *testing.T) { + std.TestSetRealm(gsaRealm) + std.TestSetOrigCaller(gsa) + + bar.Approve(a2u(consts.LAUNCHPAD_ADDR), uint64(1_000_000_000)) + std.TestSkipHeights(1) + + projectId := CreateProject( + barPath, + projectAddr02, + uint64(1_000_000_000), // 1000000000 + "", + "", + uint64(10), // 100000000 + uint64(20), // 200000000 + uint64(70), // 700000000 + uint64(time.Now().Unix()+10), // 10s later + ) + shouldEQ(t, projectId, `gno.land/r/onbloc/bar:126`) + std.TestSkipHeights(1) +} + +func TestDepositGnsToTierProject01_Tier30(t *testing.T) { + t.Run("deposit to obl project, tier 30", func(t *testing.T) { + std.TestSetRealm(gsaRealm) + std.TestSetOrigCaller(gsa) + gns.Transfer(a2u(user01), uint64(1_000_000)) // to deposit + // transfer some grc20 tokens to bypass project condition + foo.Transfer(a2u(user01), uint64(10)) + bar.Transfer(a2u(user01), uint64(10)) + + // user01 makes deposit + std.TestSetRealm(user01Realm) + std.TestSetOrigCaller(user01) + gns.Approve(a2u(consts.LAUNCHPAD_ADDR), uint64(1_000_000)) + + // skip some blocks to make project active + std.TestSkipHeights(4) + depositId := DepositGns("gno.land/r/onbloc/obl:124:30", uint64(1_000_000)) // 1000000 + shouldEQ(t, depositId, `gno.land/r/onbloc/obl:124:30:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:131`) + std.TestSkipHeights(1) + }) + + t.Run("check deposit's project token", func(t *testing.T) { + got := ApiGetDepositRewardByDepositId("gno.land/r/onbloc/obl:124:30:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:131") + shouldEQ(t, got, uint64(0)) // reward occured, but not claimable yet + }) + + t.Run("check project's recipient protocol_fee reward", func(t *testing.T) { + got := ApiGetProjectRecipientRewardByAddress(projectAddr.String()) + shouldEQ(t, got, `{"height":"132","now":"1234567908","emissionReward":"0","protocolFees":[{"tokenPath":"gno.land/r/onbloc/bar","amount":"1000"},{"tokenPath":"gno.land/r/onbloc/qux","amount":"2500"}]}`) + }) + + t.Run("check claimable deposit", func(t *testing.T) { + got := ApiGetClaimableDepositByAddress(user01.String()) + shouldEQ(t, got, uint64(0)) + }) +} + +func TestCollectProtocolFeeByProject01Recipient(t *testing.T) { + t.Run("check project's recipient protocol_fee reward", func(t *testing.T) { + got := ApiGetProjectRecipientRewardByAddress(projectAddr.String()) + shouldEQ(t, got, `{"height":"132","now":"1234567908","emissionReward":"0","protocolFees":[{"tokenPath":"gno.land/r/onbloc/bar","amount":"1000"},{"tokenPath":"gno.land/r/onbloc/qux","amount":"2500"}]}`) + }) + + t.Run("claim protocol reward", func(t *testing.T) { + std.TestSetRealm(projectRealm) + std.TestSetOrigCaller(projectAddr) + + oldBar := bar.BalanceOf(a2u(projectAddr)) + oldQux := qux.BalanceOf(a2u(projectAddr)) + shouldEQ(t, oldBar, uint64(0)) + shouldEQ(t, oldQux, uint64(0)) + + // check claimble reward + res := ApiGetProjectRecipientRewardByAddress(projectAddr.String()) + shouldEQ(t, res, `{"height":"132","now":"1234567908","emissionReward":"0","protocolFees":[{"tokenPath":"gno.land/r/onbloc/bar","amount":"1000"},{"tokenPath":"gno.land/r/onbloc/qux","amount":"2500"}]}`) + // bar 1000 + // qux 2500 + + CollectProtocolFee() + + newBar := bar.BalanceOf(a2u(projectAddr)) + newQux := qux.BalanceOf(a2u(projectAddr)) + shouldEQ(t, newBar, uint64(1000)) + shouldEQ(t, newQux, uint64(2500)) + + std.TestSkipHeights(10) // pass some blocks + }) + + t.Run("check project's recipient protocol_fee reward after claiming it", func(t *testing.T) { + got := ApiGetProjectRecipientRewardByAddress(projectAddr.String()) + shouldEQ(t, got, `{"height":"142","now":"1234567928","emissionReward":"0","protocolFees":[]}`) + }) +} + +func TestApiGetClaimableDepositByAddress(t *testing.T) { + t.Run("tier 30days isn't over", func(t *testing.T) { + got := ApiGetClaimableDepositByAddress(user01.String()) + shouldEQ(t, got, uint64(0)) + }) + + t.Run("tier 30days is over", func(t *testing.T) { + std.TestSkipHeights(int64(TIMESTAMP_30DAYS) / 2) + got := ApiGetClaimableDepositByAddress(user01.String()) + shouldEQ(t, got, uint64(1_000_000)) + + // and actual collect + std.TestSetRealm(user01Realm) + std.TestSetOrigCaller(user01) + claimed := CollectDepositGns() + shouldEQ(t, claimed, uint64(1_000_000)) + }) + + t.Run("check after collect deposit", func(t *testing.T) { + got := ApiGetClaimableDepositByAddress(user01.String()) + shouldEQ(t, got, uint64(0)) + + std.TestSetRealm(user01Realm) + std.TestSetOrigCaller(user01) + claimed := CollectDepositGns() + shouldEQ(t, claimed, uint64(0)) + }) +} diff --git a/launchpad/tests/__TEST_launchpad_deposit_project_single_recipient_test.gnoA b/launchpad/tests/__TEST_launchpad_deposit_project_single_recipient_test.gnoA new file mode 100644 index 00000000..eba7fae9 --- /dev/null +++ b/launchpad/tests/__TEST_launchpad_deposit_project_single_recipient_test.gnoA @@ -0,0 +1,144 @@ +package launchpad + +import ( + "std" + "testing" + "time" + + "gno.land/p/demo/testutils" + + "gno.land/r/gnoswap/v2/consts" + gs "gno.land/r/gnoswap/v2/gov/staker" + + "gno.land/r/onbloc/bar" + "gno.land/r/onbloc/foo" + "gno.land/r/onbloc/obl" + "gno.land/r/onbloc/qux" + + "gno.land/r/gnoswap/v2/gns" +) + +var ( + projectAddr = testutils.TestAddress("projectAddr") + user01 = testutils.TestAddress("user01") + + projectRealm = std.NewUserRealm(projectAddr) + user01Realm = std.NewUserRealm(user01) +) + +func init() { + println("test_init") +} + +func TestMockProtocolFee(t *testing.T) { + // admin > protocol_fee + // send qux, bar for testing + std.TestSetRealm(gsaRealm) + bar.Transfer(a2u(consts.PROTOCOL_FEE_ADDR), 1000) + qux.Transfer(a2u(consts.PROTOCOL_FEE_ADDR), 2500) + + shouldEQ(t, bar.BalanceOf(a2u(consts.PROTOCOL_FEE_ADDR)), uint64(1000)) + shouldEQ(t, bar.BalanceOf(a2u(consts.DEV_OPS)), uint64(0)) + shouldEQ(t, bar.BalanceOf(a2u(consts.GOV_STAKER_ADDR)), uint64(0))ㄴ + + shouldEQ(t, qux.BalanceOf(a2u(consts.PROTOCOL_FEE_ADDR)), uint64(2500)) + shouldEQ(t, qux.BalanceOf(a2u(consts.DEV_OPS)), uint64(0)) + shouldEQ(t, qux.BalanceOf(a2u(consts.GOV_STAKER_ADDR)), uint64(0)) +} + +func TestCreateProject(t *testing.T) { + std.TestSetRealm(gsaRealm) + std.TestSetOrigCaller(gsa) + + obl.Approve(a2u(consts.LAUNCHPAD_ADDR), uint64(1_000_000_000)) + std.TestSkipHeights(1) + + projectId := CreateProject( + oblPath, + projectAddr, + uint64(1_000_000_000), // 1000000000 + "gno.land/r/onbloc/foo*PAD*gno.land/r/onbloc/bar", + "1*PAD*2", + uint64(10), // 100000000 + uint64(20), // 200000000 + uint64(70), // 700000000 + uint64(time.Now().Unix()+10), // 10s later + ) + shouldEQ(t, projectId, `gno.land/r/onbloc/obl:124`) + std.TestSkipHeights(1) +} + +func TestDepositGnsToTier30(t *testing.T) { + std.TestSetRealm(gsaRealm) + std.TestSetOrigCaller(gsa) + gns.Transfer(a2u(user01), uint64(1_000_000)) // to deposit + // transfer some grc20 tokens to bypass project condition + foo.Transfer(a2u(user01), uint64(10)) + bar.Transfer(a2u(user01), uint64(10)) + + std.TestSetRealm(user01Realm) + std.TestSetOrigCaller(user01) + gns.Approve(a2u(consts.LAUNCHPAD_ADDR), uint64(1_000_000)) + + // skip some blocks to make project active + std.TestSkipHeights(4) + depositId := DepositGns("gno.land/r/onbloc/obl:124:30", uint64(1_000_000)) // 1000000 + shouldEQ(t, depositId, `gno.land/r/onbloc/obl:124:30:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) + std.TestSkipHeights(1) +} + +func TestCollectProtocolFee(t *testing.T) { + std.TestSetRealm(projectRealm) + std.TestSetOrigCaller(projectAddr) + + oldBar := bar.BalanceOf(a2u(projectAddr)) + oldQux := qux.BalanceOf(a2u(projectAddr)) + shouldEQ(t, oldBar, uint64(0)) + shouldEQ(t, oldQux, uint64(0)) + + // check claimble reward + res := gs.GetClaimableRewardByAddress(projectAddr.String()) + shouldEQ(t, res, `{"height":"130","now":"1234567904","emissionReward":"0","protocolFees":[{"tokenPath":"gno.land/r/onbloc/bar","amount":"1000"},{"tokenPath":"gno.land/r/onbloc/qux","amount":"2500"}]}`) + + CollectProtocolFee() + + newBar := bar.BalanceOf(a2u(projectAddr)) + newQux := qux.BalanceOf(a2u(projectAddr)) + shouldEQ(t, newBar, uint64(1000)) + shouldEQ(t, newQux, uint64(2500)) + + std.TestSkipHeights(10) // pass some blocks +} + +func TestCollectDepositGns(t *testing.T) { + std.TestSetRealm(user01Realm) + std.TestSetOrigCaller(user01) + + t.Run("claim before 30 days", func(t *testing.T) { + claimed := CollectDepositGns() + std.TestSkipHeights(1) + shouldEQ(t, claimed, uint64(0)) + println() + println() + }) + + t.Run("claim after 30 days", func(t *testing.T) { + std.TestSkipHeights(int64(TIMESTAMP_30DAYS) / 2) + claimed := CollectDepositGns() + shouldEQ(t, claimed, uint64(1_000_000)) + println() + println() + }) + + t.Run("no more claim in same block", func(t *testing.T) { + claimed := CollectDepositGns() + std.TestSkipHeights(1) + shouldEQ(t, claimed, uint64(0)) + println() + println() + }) + + // check claimble reward + res := gs.GetClaimableRewardByAddress(projectAddr.String()) + shouldEQ(t, res, `{"height":"1296142","now":"1237159928","emissionReward":"0","protocolFees":[]}`) +} diff --git a/launchpad/tests/__TEST_launchpad_deposit_project_two_recipient_test.gnoA b/launchpad/tests/__TEST_launchpad_deposit_project_two_recipient_test.gnoA new file mode 100644 index 00000000..41fab35a --- /dev/null +++ b/launchpad/tests/__TEST_launchpad_deposit_project_two_recipient_test.gnoA @@ -0,0 +1,184 @@ +package launchpad + +import ( + "std" + "testing" + "time" + + "gno.land/p/demo/testutils" + + "gno.land/r/gnoswap/v2/consts" + gs "gno.land/r/gnoswap/v2/gov/staker" + + "gno.land/r/onbloc/bar" + "gno.land/r/onbloc/foo" + "gno.land/r/onbloc/obl" + "gno.land/r/onbloc/qux" + + "gno.land/r/gnoswap/v2/gns" +) + +var ( + projectAddr = testutils.TestAddress("projectAddr") + projectAddr02 = testutils.TestAddress("projectAddr02") + user01 = testutils.TestAddress("user01") + + projectRealm = std.NewUserRealm(projectAddr) + projectRealm02 = std.NewUserRealm(projectAddr02) + user01Realm = std.NewUserRealm(user01) +) + +func init() { + println("test_init") +} + +func TestMockProtocolFee(t *testing.T) { + // admin > protocol_fee + // send qux, bar for testing + std.TestSetRealm(gsaRealm) + bar.Transfer(a2u(consts.PROTOCOL_FEE_ADDR), 1000) + qux.Transfer(a2u(consts.PROTOCOL_FEE_ADDR), 2500) + + shouldEQ(t, bar.BalanceOf(a2u(consts.PROTOCOL_FEE_ADDR)), uint64(1000)) + shouldEQ(t, bar.BalanceOf(a2u(consts.DEV_OPS)), uint64(0)) + shouldEQ(t, bar.BalanceOf(a2u(consts.GOV_STAKER_ADDR)), uint64(0)) + + shouldEQ(t, qux.BalanceOf(a2u(consts.PROTOCOL_FEE_ADDR)), uint64(2500)) + shouldEQ(t, qux.BalanceOf(a2u(consts.DEV_OPS)), uint64(0)) + shouldEQ(t, qux.BalanceOf(a2u(consts.GOV_STAKER_ADDR)), uint64(0)) +} + +func TestCreateProject(t *testing.T) { + std.TestSetRealm(gsaRealm) + std.TestSetOrigCaller(gsa) + + obl.Approve(a2u(consts.LAUNCHPAD_ADDR), uint64(1_000_000_000)) + std.TestSkipHeights(1) + + projectId := CreateProject( + oblPath, + projectAddr, + uint64(1_000_000_000), // 1000000000 + "gno.land/r/onbloc/foo*PAD*gno.land/r/onbloc/bar", + "1*PAD*2", + uint64(10), // 100000000 + uint64(20), // 200000000 + uint64(70), // 700000000 + uint64(time.Now().Unix()+10), // 10s later + ) + shouldEQ(t, projectId, `gno.land/r/onbloc/obl:124`) + std.TestSkipHeights(1) +} + +func TestCreateProject02(t *testing.T) { + std.TestSetRealm(gsaRealm) + std.TestSetOrigCaller(gsa) + + bar.Approve(a2u(consts.LAUNCHPAD_ADDR), uint64(1_000_000_000)) + std.TestSkipHeights(1) + + projectId := CreateProject( + barPath, + projectAddr02, + uint64(1_000_000_000), // 1000000000 + "", + "", + uint64(10), // 100000000 + uint64(20), // 200000000 + uint64(70), // 700000000 + uint64(time.Now().Unix()+10), // 10s later + ) + shouldEQ(t, projectId, `gno.land/r/onbloc/bar:126`) + std.TestSkipHeights(1) +} + +func TestDepositGnsToTierProject01_Tier30(t *testing.T) { + std.TestSetRealm(gsaRealm) + std.TestSetOrigCaller(gsa) + gns.Transfer(a2u(user01), uint64(1_000_000)) // to deposit + // transfer some grc20 tokens to bypass project condition + foo.Transfer(a2u(user01), uint64(10)) + bar.Transfer(a2u(user01), uint64(10)) + + std.TestSetRealm(user01Realm) + std.TestSetOrigCaller(user01) + gns.Approve(a2u(consts.LAUNCHPAD_ADDR), uint64(1_000_000)) + + // skip some blocks to make project active + std.TestSkipHeights(4) + depositId := DepositGns("gno.land/r/onbloc/obl:124:30", uint64(1_000_000)) // 1000000 + shouldEQ(t, depositId, `gno.land/r/onbloc/obl:124:30:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:131`) + std.TestSkipHeights(1) +} + +func TestDepositGnsToTierProject02_Tier180(t *testing.T) { + std.TestSetRealm(gsaRealm) + std.TestSetOrigCaller(gsa) + gns.Transfer(a2u(user01), uint64(9_000_000)) // to deposit + + std.TestSetRealm(user01Realm) + std.TestSetOrigCaller(user01) + gns.Approve(a2u(consts.LAUNCHPAD_ADDR), uint64(9_000_000)) + + // skip some blocks to make project active + std.TestSkipHeights(4) + depositId := DepositGns("gno.land/r/onbloc/bar:126:180", uint64(9_000_000)) // 9000000 + shouldEQ(t, depositId, `gno.land/r/onbloc/bar:126:180:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:136`) + std.TestSkipHeights(1) +} + +func TestCollectProtocolFeeByProject01Recipient(t *testing.T) { + std.TestSetRealm(projectRealm) + std.TestSetOrigCaller(projectAddr) + + oldBar := bar.BalanceOf(a2u(projectAddr)) + oldQux := qux.BalanceOf(a2u(projectAddr)) + shouldEQ(t, oldBar, uint64(0)) + shouldEQ(t, oldQux, uint64(0)) + + // check claimble reward + res := gs.GetClaimableRewardByAddress(projectAddr.String()) + shouldEQ(t, res, `{"height":"137","now":"1234567918","emissionReward":"0","protocolFees":[{"tokenPath":"gno.land/r/onbloc/bar","amount":"189"},{"tokenPath":"gno.land/r/onbloc/qux","amount":"474"}]}`) + // shouldEQ(t, res, `{"height":"132","now":"1234567908","emissionReward":"0","protocolFees":[{"tokenPath":"gno.land/r/onbloc/bar","amount":"1000"},{"tokenPath":"gno.land/r/onbloc/qux","amount":"2500"}]}`) + + CollectProtocolFee() + + newBar := bar.BalanceOf(a2u(projectAddr)) + newQux := qux.BalanceOf(a2u(projectAddr)) + shouldEQ(t, newBar, uint64(189)) + shouldEQ(t, newQux, uint64(474)) + + std.TestSkipHeights(10) // pass some blocks +} + +func TestCollectProtocolFeeByProject02Recipient(t *testing.T) { + std.TestSetRealm(projectRealm02) + std.TestSetOrigCaller(projectAddr02) + + oldBar := bar.BalanceOf(a2u(projectAddr02)) + oldQux := qux.BalanceOf(a2u(projectAddr02)) + shouldEQ(t, oldBar, uint64(0)) + shouldEQ(t, oldQux, uint64(0)) + + // check claimble reward + res := gs.GetClaimableRewardByAddress(projectAddr02.String()) + shouldEQ(t, res, `{"height":"147","now":"1234567938","emissionReward":"0","protocolFees":[{"tokenPath":"gno.land/r/onbloc/bar","amount":"810"},{"tokenPath":"gno.land/r/onbloc/qux","amount":"2025"}]}`) + + CollectProtocolFee() + + newBar := bar.BalanceOf(a2u(projectAddr02)) + newQux := qux.BalanceOf(a2u(projectAddr02)) + shouldEQ(t, newBar, uint64(810)) + shouldEQ(t, newQux, uint64(2025)) + + std.TestSkipHeights(10) // pass some blocks +} + +func TestCheckClaimable(t *testing.T) { + // check claimble reward + claimableProject01 := gs.GetClaimableRewardByAddress(projectAddr.String()) + shouldEQ(t, claimableProject01, `{"height":"157","now":"1234567958","emissionReward":"0","protocolFees":[]}`) + + claimableProject02 := gs.GetClaimableRewardByAddress(projectAddr02.String()) + shouldEQ(t, claimableProject02, `{"height":"157","now":"1234567958","emissionReward":"0","protocolFees":[]}`) +} diff --git a/launchpad/tests/__TEST_launchpad_tier180_single_deposit_reward_by_proejct_tier_test.gnoA b/launchpad/tests/__TEST_launchpad_tier180_single_deposit_reward_by_proejct_tier_test.gnoA new file mode 100644 index 00000000..077d31a1 --- /dev/null +++ b/launchpad/tests/__TEST_launchpad_tier180_single_deposit_reward_by_proejct_tier_test.gnoA @@ -0,0 +1,124 @@ +package launchpad + +import ( + "std" + "testing" + "time" + + "gno.land/p/demo/testutils" + + "gno.land/r/gnoswap/v2/consts" + + "gno.land/r/onbloc/bar" + "gno.land/r/onbloc/foo" + "gno.land/r/onbloc/obl" + + "gno.land/r/gnoswap/v2/gns" +) + +var ( + projectAddr = testutils.TestAddress("projectAddr") + user01 = testutils.TestAddress("user01") + + projectRealm = std.NewUserRealm(projectAddr) + user01Realm = std.NewUserRealm(user01) +) + +func init() { + println("test_init") +} + +func TestCreateProject(t *testing.T) { + std.TestSetRealm(gsaRealm) + std.TestSetOrigCaller(gsa) + + obl.Approve(a2u(consts.LAUNCHPAD_ADDR), uint64(1_000_000_000)) + std.TestSkipHeights(1) + + projectId := CreateProject( + oblPath, + projectAddr, + uint64(1_000_000_000), // 1000000000 + "gno.land/r/onbloc/foo*PAD*gno.land/r/onbloc/bar", + "1*PAD*2", + uint64(10), // 100000000 + uint64(20), // 200000000 + uint64(70), // 700000000 + uint64(time.Now().Unix()+10), // 10s later + ) + shouldEQ(t, projectId, `gno.land/r/onbloc/obl:124`) + std.TestSkipHeights(1) +} + +func TestDepositGnsToTier90(t *testing.T) { + std.TestSetRealm(gsaRealm) + std.TestSetOrigCaller(gsa) + gns.Transfer(a2u(user01), uint64(1_000_000)) // to deposit + // transfer some grc20 tokens to bypass project condition + foo.Transfer(a2u(user01), uint64(10)) + bar.Transfer(a2u(user01), uint64(10)) + + std.TestSetRealm(user01Realm) + std.TestSetOrigCaller(user01) + gns.Approve(a2u(consts.LAUNCHPAD_ADDR), uint64(1_000_000)) + + // skip some blocks to make project active + std.TestSkipHeights(4) + depositId := DepositGns("gno.land/r/onbloc/obl:124:90", uint64(1_000_000)) + shouldEQ(t, depositId, `gno.land/r/onbloc/obl:124:90:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) + std.TestSkipHeights(1) +} + +func TestCollectRewardByDepositId(t *testing.T) { + std.TestSetRealm(user01Realm) + std.TestSetOrigCaller(user01) + + t.Run("claim reward before 7 days(for 90day tier's init reward)", func(t *testing.T) { + reward := CollectRewardByDepositId(`gno.land/r/onbloc/obl:124:90:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) + shouldEQ(t, reward, uint64(0)) + std.TestSkipHeights(1) + println() + }) + + t.Run("claim reward before 7 days(for 90day tier's init reward)", func(t *testing.T) { + std.TestSkipHeights(123) + reward := CollectRewardByDepositId(`gno.land/r/onbloc/obl:124:90:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) + shouldEQ(t, reward, uint64(0)) + std.TestSkipHeights(1) + println() + }) + + t.Run("claim after 7 days", func(t *testing.T) { + std.TestSkipHeights(int64(TIMESTAMP_7DAYS) / 2) + reward := CollectRewardByDepositId(`gno.land/r/onbloc/obl:124:90:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) + shouldEQ(t, reward, uint64(15562035)) + println() + }) + + t.Run("no more claim in same block", func(t *testing.T) { + reward := CollectRewardByDepositId(`gno.land/r/onbloc/obl:124:90:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) + shouldEQ(t, reward, uint64(0)) + println() + }) + + t.Run("wait 1 more block, then claim", func(t *testing.T) { + std.TestSkipHeights(1) + reward := CollectRewardByDepositId(`gno.land/r/onbloc/obl:124:90:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) + shouldEQ(t, reward, uint64(51)) + println() + }) + + t.Run("90day tier is over", func(t *testing.T) { + std.TestSkipHeights(int64(TIMESTAMP_90DAYS) / 2) + reward := CollectRewardByDepositId(`gno.land/r/onbloc/obl:124:90:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) + shouldEQ(t, reward, uint64(184437911)) + println() + }) + + t.Run("more block after 90 days", func(t *testing.T) { + std.TestSkipHeights(int64(TIMESTAMP_90DAYS) / 2) + reward := CollectRewardByDepositId(`gno.land/r/onbloc/obl:124:90:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) + shouldEQ(t, reward, uint64(0)) + println() + }) +} diff --git a/launchpad/tests/__TEST_launchpad_tier30_single_deposit_01_deposit_collect_gns_test.gnoA b/launchpad/tests/__TEST_launchpad_tier30_single_deposit_01_deposit_collect_gns_test.gnoA new file mode 100644 index 00000000..a2f25cd4 --- /dev/null +++ b/launchpad/tests/__TEST_launchpad_tier30_single_deposit_01_deposit_collect_gns_test.gnoA @@ -0,0 +1,256 @@ +package launchpad + +import ( + "std" + "testing" + "time" + + "gno.land/p/demo/testutils" + + "gno.land/r/gnoswap/v2/consts" + + "gno.land/r/onbloc/bar" + "gno.land/r/onbloc/foo" + "gno.land/r/onbloc/obl" + + "gno.land/r/gnoswap/v2/gns" +) + +var ( + projectAddr = testutils.TestAddress("projectAddr") + user01 = testutils.TestAddress("user01") + + projectRealm = std.NewUserRealm(projectAddr) + user01Realm = std.NewUserRealm(user01) +) + +func init() { + println("test_init") +} + +func TestCreateProject(t *testing.T) { + std.TestSetRealm(gsaRealm) + std.TestSetOrigCaller(gsa) + + obl.Approve(a2u(consts.LAUNCHPAD_ADDR), uint64(1_000_000_000)) + std.TestSkipHeights(1) + + { + // check before project create + shouldEQ(t, len(projects), 0) + shouldEQ(t, len(deposits), 0) + shouldEQ(t, len(depositsByProject), 0) + shouldEQ(t, len(depositsByUser), 0) + } + + projectId := CreateProject( + oblPath, + projectAddr, + uint64(1_000_000_000), // 1000000000 + "gno.land/r/onbloc/foo*PAD*gno.land/r/onbloc/bar", + "1*PAD*2", + uint64(10), // 100000000 + uint64(20), // 200000000 + uint64(70), // 700000000 + uint64(time.Now().Unix()+10), // 10s later + ) + shouldEQ(t, projectId, `gno.land/r/onbloc/obl:124`) + std.TestSkipHeights(1) + + { + // check after project create + shouldEQ(t, len(projects), 1) + + project := projects[`gno.land/r/onbloc/obl:124`] + shouldEQ(t, project.id, `gno.land/r/onbloc/obl:124`) + shouldEQ(t, project.tokenPath, `gno.land/r/onbloc/obl`) + shouldEQ(t, project.depositAmount, uint64(1000000000)) + shouldEQ(t, project.recipient, projectAddr) + + shouldEQ(t, project.conditions[`gno.land/r/onbloc/foo`].minAmount, uint64(1)) + shouldEQ(t, project.conditions[`gno.land/r/onbloc/bar`].minAmount, uint64(2)) + + shouldEQ(t, project.tier30Ratio, uint64(10)) + tier30 := project.tier30 + shouldEQ(t, tier30.id, `gno.land/r/onbloc/obl:124:30`) + shouldEQ(t, tier30.collectWaitDuration, uint64(129600)) // block range for 3 days based on 2s per block + shouldEQ(t, tier30.tierAmount, uint64(100000000)) + shouldNEQ(t, tier30.tierAmountPerBlockX96.ToString(), `0`) + shouldEQ(t, tier30.endTime, project.startTime+TIMESTAMP_30DAYS) + shouldEQ(t, tier30.depositAmount, uint64(0)) + shouldEQ(t, tier30.participant, uint64(0)) + + shouldEQ(t, project.tier90Ratio, uint64(20)) + tier90 := project.tier90 + shouldEQ(t, tier90.id, `gno.land/r/onbloc/obl:124:90`) + shouldEQ(t, tier90.collectWaitDuration, uint64(302400)) // block range for 7 days based on 2s per block + shouldEQ(t, tier90.tierAmount, uint64(200000000)) + shouldNEQ(t, tier90.tierAmountPerBlockX96.ToString(), `0`) + shouldEQ(t, tier90.endTime, project.startTime+TIMESTAMP_90DAYS) + shouldEQ(t, tier90.depositAmount, uint64(0)) + shouldEQ(t, tier90.participant, uint64(0)) + + shouldEQ(t, project.tier180Ratio, uint64(70)) + tier180 := project.tier180 + shouldEQ(t, tier180.id, `gno.land/r/onbloc/obl:124:180`) + shouldEQ(t, tier180.collectWaitDuration, uint64(604800)) // block range for 7 days based on 2s per block + shouldEQ(t, tier180.tierAmount, uint64(700000000)) + shouldNEQ(t, tier180.tierAmountPerBlockX96.ToString(), `0`) + shouldEQ(t, tier180.endTime, project.startTime+TIMESTAMP_180DAYS) + shouldEQ(t, tier180.depositAmount, uint64(0)) + shouldEQ(t, tier180.participant, uint64(0)) + + shouldEQ(t, project.createdHeight, uint64(124)) + shouldEQ(t, project.createdTime, uint64(1234567892)) + + shouldEQ(t, project.startTime, uint64(1234567902)) + shouldEQ(t, project.totalDepositAmount, uint64(0)) + shouldEQ(t, project.totalParticipant, uint64(0)) + + shouldEQ(t, len(deposits), 0) + shouldEQ(t, len(depositsByProject), 0) + shouldEQ(t, len(depositsByUser), 0) + } +} + +func TestDepositGnsToTier30(t *testing.T) { + std.TestSetRealm(gsaRealm) + std.TestSetOrigCaller(gsa) + gns.Transfer(a2u(user01), uint64(1_000_000)) // to deposit + // transfer some grc20 tokens to bypass project condition + foo.Transfer(a2u(user01), uint64(10)) + bar.Transfer(a2u(user01), uint64(10)) + + std.TestSetRealm(user01Realm) + std.TestSetOrigCaller(user01) + gns.Approve(a2u(consts.LAUNCHPAD_ADDR), uint64(1_000_000)) + + { + // check before deposit + shouldEQ(t, len(projects), 1) + + project := projects[`gno.land/r/onbloc/obl:124`] + shouldEQ(t, project.tier30Ratio, uint64(10)) + + tier30 := project.tier30 + shouldEQ(t, tier30.id, `gno.land/r/onbloc/obl:124:30`) + shouldEQ(t, tier30.collectWaitDuration, uint64(129600)) + shouldEQ(t, tier30.tierAmount, uint64(100000000)) + shouldNEQ(t, tier30.tierAmountPerBlockX96.ToString(), `0`) + shouldEQ(t, tier30.endTime, project.startTime+TIMESTAMP_30DAYS) + shouldEQ(t, tier30.depositAmount, uint64(0)) + shouldEQ(t, tier30.participant, uint64(0)) + + shouldEQ(t, len(deposits), 0) + shouldEQ(t, len(depositsByProject), 0) + shouldEQ(t, len(depositsByUser), 0) + } + + // skip some blocks to make project active + std.TestSkipHeights(4) + depositId := DepositGns("gno.land/r/onbloc/obl:124:30", uint64(1_000_000)) // 1000000 + shouldEQ(t, depositId, `gno.land/r/onbloc/obl:124:30:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) + std.TestSkipHeights(1) + + { + // check after deposit + shouldEQ(t, len(projects), 1) + + project := projects[`gno.land/r/onbloc/obl:124`] + shouldEQ(t, project.tier30Ratio, uint64(10)) + + tier30 := project.tier30 + shouldEQ(t, tier30.id, `gno.land/r/onbloc/obl:124:30`) + shouldEQ(t, tier30.collectWaitDuration, uint64(129600)) + shouldEQ(t, tier30.tierAmount, uint64(100000000)) + shouldNEQ(t, tier30.tierAmountPerBlockX96.ToString(), `0`) + shouldEQ(t, tier30.endTime, project.startTime+TIMESTAMP_30DAYS) + shouldEQ(t, tier30.depositAmount, uint64(1000000)) + shouldEQ(t, tier30.participant, uint64(1)) + + // + shouldEQ(t, len(deposits), 1) + deposit := deposits[`gno.land/r/onbloc/obl:124:30:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`] + shouldEQ(t, deposit.id, `gno.land/r/onbloc/obl:124:30:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) + shouldEQ(t, deposit.projectId, `gno.land/r/onbloc/obl:124`) + shouldEQ(t, deposit.tier, `30`) + shouldEQ(t, deposit.depositor, user01) + shouldEQ(t, deposit.amount, uint64(1000000)) + shouldEQ(t, deposit.depositHeight, uint64(129)) + shouldEQ(t, deposit.depositTime, uint64(1234567902)) + shouldEQ(t, deposit.depositCollectHeight, uint64(0)) + shouldEQ(t, deposit.depositCollectTime, uint64(0)) + shouldEQ(t, deposit.rewardAmount, uint64(0)) + shouldEQ(t, deposit.rewardCollectHeight, uint64(0)) + shouldEQ(t, deposit.rewardCollectTime, uint64(0)) + + // + shouldEQ(t, len(depositsByProject), 1) + shouldEQ(t, depositsByProject[`gno.land/r/onbloc/obl:124`][`30`][0], `gno.land/r/onbloc/obl:124:30:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) + + // + shouldEQ(t, len(depositsByUser), 1) + shouldEQ(t, depositsByUser[`g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv`][0], `gno.land/r/onbloc/obl:124:30:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) + } +} + +func TestCollectDepositGns(t *testing.T) { + std.TestSetRealm(user01Realm) + std.TestSetOrigCaller(user01) + + t.Run("claim before 30 days", func(t *testing.T) { + claimed := CollectDepositGns() + std.TestSkipHeights(1) + shouldEQ(t, claimed, uint64(0)) + println() + println() + }) + + t.Run("claim after 30 days", func(t *testing.T) { + std.TestSkipHeights(int64(TIMESTAMP_30DAYS) / 2) + claimed := CollectDepositGns() + shouldEQ(t, claimed, uint64(1_000_000)) + println() + println() + }) + + t.Run("no more claim in same block", func(t *testing.T) { + claimed := CollectDepositGns() + std.TestSkipHeights(1) + shouldEQ(t, claimed, uint64(0)) + println() + println() + }) + + // check deposit + shouldEQ(t, len(deposits), 1) + deposit := deposits[`gno.land/r/onbloc/obl:124:30:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`] + shouldEQ(t, deposit.id, `gno.land/r/onbloc/obl:124:30:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) + shouldEQ(t, deposit.projectId, `gno.land/r/onbloc/obl:124`) + shouldEQ(t, deposit.tier, `30`) + shouldEQ(t, deposit.depositor, user01) + shouldEQ(t, deposit.amount, uint64(1000000)) + shouldEQ(t, deposit.depositHeight, uint64(129)) + shouldEQ(t, deposit.depositTime, uint64(1234567902)) + shouldEQ(t, deposit.depositCollectHeight, uint64(1296131)) + shouldEQ(t, deposit.depositCollectTime, uint64(1237159906)) + shouldEQ(t, deposit.rewardAmount, uint64(99999999)) + shouldEQ(t, deposit.rewardCollectHeight, uint64(0)) + shouldEQ(t, deposit.rewardCollectTime, uint64(0)) + + // + shouldEQ(t, len(depositsByProject), 1) + shouldEQ(t, depositsByProject[`gno.land/r/onbloc/obl:124`][`30`][0], `gno.land/r/onbloc/obl:124:30:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) + + // + shouldEQ(t, len(depositsByUser), 1) + shouldEQ(t, depositsByUser[`g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv`][0], `gno.land/r/onbloc/obl:124:30:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) + + // check project + project := projects[`gno.land/r/onbloc/obl:124`] + tier30 := project.tier30 + shouldEQ(t, tier30.id, `gno.land/r/onbloc/obl:124:30`) + shouldEQ(t, tier30.tierAmount, uint64(100000000)) + shouldEQ(t, tier30.depositAmount, uint64(0)) + shouldEQ(t, tier30.participant, uint64(0)) +} diff --git a/launchpad/tests/__TEST_launchpad_tier30_single_deposit_02_deposit_reward_test.gnoA b/launchpad/tests/__TEST_launchpad_tier30_single_deposit_02_deposit_reward_test.gnoA new file mode 100644 index 00000000..51f20d28 --- /dev/null +++ b/launchpad/tests/__TEST_launchpad_tier30_single_deposit_02_deposit_reward_test.gnoA @@ -0,0 +1,117 @@ +package launchpad + +import ( + "std" + "testing" + "time" + + "gno.land/p/demo/testutils" + + "gno.land/r/gnoswap/v2/consts" + + "gno.land/r/onbloc/bar" + "gno.land/r/onbloc/foo" + "gno.land/r/onbloc/obl" + + "gno.land/r/gnoswap/v2/gns" +) + +var ( + projectAddr = testutils.TestAddress("projectAddr") + user01 = testutils.TestAddress("user01") + + projectRealm = std.NewUserRealm(projectAddr) + user01Realm = std.NewUserRealm(user01) +) + +func init() { + println("test_init") +} + +func TestCreateProject(t *testing.T) { + std.TestSetRealm(gsaRealm) + std.TestSetOrigCaller(gsa) + + obl.Approve(a2u(consts.LAUNCHPAD_ADDR), uint64(1_000_000_000)) + std.TestSkipHeights(1) + + projectId := CreateProject( + oblPath, + projectAddr, + uint64(1_000_000_000), // 1000000000 + "gno.land/r/onbloc/foo*PAD*gno.land/r/onbloc/bar", + "1*PAD*2", + uint64(10), // 100000000 + uint64(20), // 200000000 + uint64(70), // 700000000 + uint64(time.Now().Unix()+10), // 10s later + ) + shouldEQ(t, projectId, `gno.land/r/onbloc/obl:124`) + std.TestSkipHeights(1) + +} + +func TestDepositGnsToTier30(t *testing.T) { + std.TestSetRealm(gsaRealm) + std.TestSetOrigCaller(gsa) + gns.Transfer(a2u(user01), uint64(1_000_000)) // to deposit + // transfer some grc20 tokens to bypass project condition + foo.Transfer(a2u(user01), uint64(10)) + bar.Transfer(a2u(user01), uint64(10)) + + std.TestSetRealm(user01Realm) + std.TestSetOrigCaller(user01) + gns.Approve(a2u(consts.LAUNCHPAD_ADDR), uint64(1_000_000)) + + // skip some blocks to make project active + std.TestSkipHeights(4) + depositId := DepositGns("gno.land/r/onbloc/obl:124:30", uint64(1_000_000)) + shouldEQ(t, depositId, `gno.land/r/onbloc/obl:124:30:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) + std.TestSkipHeights(1) +} + +func TestCollectReward(t *testing.T) { + std.TestSetRealm(user01Realm) + std.TestSetOrigCaller(user01) + + t.Run("claim reward before 30 days", func(t *testing.T) { + CollectReward() + std.TestSkipHeights(1) + println() + }) + + t.Run("claim after 30 days", func(t *testing.T) { + std.TestSkipHeights(int64(TIMESTAMP_30DAYS) / 2) + CollectReward() + println() + }) + + t.Run("no more claim in same block", func(t *testing.T) { + CollectReward() + println() + }) + + // + shouldEQ(t, len(deposits), 1) + deposit := deposits[`gno.land/r/onbloc/obl:124:30:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`] + shouldEQ(t, deposit.id, `gno.land/r/onbloc/obl:124:30:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) + shouldEQ(t, deposit.projectId, `gno.land/r/onbloc/obl:124`) + shouldEQ(t, deposit.tier, `30`) + shouldEQ(t, deposit.depositor, user01) + shouldEQ(t, deposit.amount, uint64(1000000)) + shouldEQ(t, deposit.depositHeight, uint64(129)) + shouldEQ(t, deposit.depositTime, uint64(1234567902)) + shouldEQ(t, deposit.depositCollectHeight, uint64(0)) + shouldEQ(t, deposit.depositCollectTime, uint64(0)) + shouldEQ(t, deposit.rewardAmount, uint64(0)) + shouldEQ(t, deposit.rewardCollectHeight, uint64(1296131)) + shouldEQ(t, deposit.rewardCollectTime, uint64(1237159906)) + + // + shouldEQ(t, len(depositsByProject), 1) + shouldEQ(t, depositsByProject[`gno.land/r/onbloc/obl:124`][`30`][0], `gno.land/r/onbloc/obl:124:30:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) + + // + shouldEQ(t, len(depositsByUser), 1) + shouldEQ(t, depositsByUser[`g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv`][0], `gno.land/r/onbloc/obl:124:30:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) +} diff --git a/launchpad/tests/__TEST_launchpad_tier30_single_deposit_03_deposit_reward_by_proejct_test.gnoA b/launchpad/tests/__TEST_launchpad_tier30_single_deposit_03_deposit_reward_by_proejct_test.gnoA new file mode 100644 index 00000000..dcc294d6 --- /dev/null +++ b/launchpad/tests/__TEST_launchpad_tier30_single_deposit_03_deposit_reward_by_proejct_test.gnoA @@ -0,0 +1,122 @@ +package launchpad + +import ( + "std" + "testing" + "time" + + "gno.land/p/demo/testutils" + + "gno.land/r/gnoswap/v2/consts" + + "gno.land/r/onbloc/bar" + "gno.land/r/onbloc/foo" + "gno.land/r/onbloc/obl" + + "gno.land/r/gnoswap/v2/gns" +) + +// var proejctAddr = testutils.TestAddress("proejctAddr") +// var projectRealm = std.NewUserRealm(proejctAddr) +var ( + projectAddr = testutils.TestAddress("projectAddr") + user01 = testutils.TestAddress("user01") + + projectRealm = std.NewUserRealm(projectAddr) + user01Realm = std.NewUserRealm(user01) +) + +func init() { + println("test_init") +} + +func TestCreateProject(t *testing.T) { + std.TestSetRealm(gsaRealm) + std.TestSetOrigCaller(gsa) + + obl.Approve(a2u(consts.LAUNCHPAD_ADDR), uint64(1_000_000_000)) + std.TestSkipHeights(1) + + projectId := CreateProject( + oblPath, + projectAddr, + uint64(1_000_000_000), // 1000000000 + "gno.land/r/onbloc/foo*PAD*gno.land/r/onbloc/bar", + "1*PAD*2", + uint64(10), // 100000000 + uint64(20), // 200000000 + uint64(70), // 700000000 + uint64(time.Now().Unix()+10), // 10s later + ) + shouldEQ(t, projectId, `gno.land/r/onbloc/obl:124`) + std.TestSkipHeights(1) + +} + +func TestDepositGnsToTier30(t *testing.T) { + std.TestSetRealm(gsaRealm) + std.TestSetOrigCaller(gsa) + gns.Transfer(a2u(user01), uint64(1_000_000)) // to deposit + // transfer some grc20 tokens to bypass project condition + foo.Transfer(a2u(user01), uint64(10)) + bar.Transfer(a2u(user01), uint64(10)) + + std.TestSetRealm(user01Realm) + std.TestSetOrigCaller(user01) + gns.Approve(a2u(consts.LAUNCHPAD_ADDR), uint64(1_000_000)) + + // skip some blocks to make project active + std.TestSkipHeights(4) + depositId := DepositGns("gno.land/r/onbloc/obl:124:30", uint64(1_000_000)) + shouldEQ(t, depositId, `gno.land/r/onbloc/obl:124:30:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) + std.TestSkipHeights(1) +} + +func TestCollectRewardByProjectId(t *testing.T) { + std.TestSetRealm(user01Realm) + std.TestSetOrigCaller(user01) + + t.Run("claim reward before 30 days", func(t *testing.T) { + reward := CollectRewardByProjectId(`gno.land/r/onbloc/obl:124`) + shouldEQ(t, reward, uint64(0)) + std.TestSkipHeights(1) + println() + }) + + t.Run("claim after 30 days", func(t *testing.T) { + std.TestSkipHeights(int64(TIMESTAMP_30DAYS) / 2) + reward := CollectRewardByProjectId(`gno.land/r/onbloc/obl:124`) + shouldEQ(t, reward, uint64(99999999)) + println() + }) + + t.Run("no more claim in same block", func(t *testing.T) { + reward := CollectRewardByProjectId(`gno.land/r/onbloc/obl:124`) + shouldEQ(t, reward, uint64(0)) + println() + }) + + // + shouldEQ(t, len(deposits), 1) + deposit := deposits[`gno.land/r/onbloc/obl:124:30:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`] + shouldEQ(t, deposit.id, `gno.land/r/onbloc/obl:124:30:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) + shouldEQ(t, deposit.projectId, `gno.land/r/onbloc/obl:124`) + shouldEQ(t, deposit.tier, `30`) + shouldEQ(t, deposit.depositor, user01) + shouldEQ(t, deposit.amount, uint64(1000000)) + shouldEQ(t, deposit.depositHeight, uint64(129)) + shouldEQ(t, deposit.depositTime, uint64(1234567902)) + shouldEQ(t, deposit.depositCollectHeight, uint64(0)) + shouldEQ(t, deposit.depositCollectTime, uint64(0)) + shouldEQ(t, deposit.rewardAmount, uint64(0)) + shouldEQ(t, deposit.rewardCollectHeight, uint64(1296131)) + shouldEQ(t, deposit.rewardCollectTime, uint64(1237159906)) + + // + shouldEQ(t, len(depositsByProject), 1) + shouldEQ(t, depositsByProject[`gno.land/r/onbloc/obl:124`][`30`][0], `gno.land/r/onbloc/obl:124:30:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) + + // + shouldEQ(t, len(depositsByUser), 1) + shouldEQ(t, depositsByUser[`g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv`][0], `gno.land/r/onbloc/obl:124:30:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) +} diff --git a/launchpad/tests/__TEST_launchpad_tier30_single_deposit_04_deposit_reward_by_proejct_tier_test.gnoA b/launchpad/tests/__TEST_launchpad_tier30_single_deposit_04_deposit_reward_by_proejct_tier_test.gnoA new file mode 100644 index 00000000..5f80525f --- /dev/null +++ b/launchpad/tests/__TEST_launchpad_tier30_single_deposit_04_deposit_reward_by_proejct_tier_test.gnoA @@ -0,0 +1,122 @@ +package launchpad + +import ( + "std" + "testing" + "time" + + "gno.land/p/demo/testutils" + + "gno.land/r/gnoswap/v2/consts" + + "gno.land/r/onbloc/bar" + "gno.land/r/onbloc/foo" + "gno.land/r/onbloc/obl" + + "gno.land/r/gnoswap/v2/gns" +) + +// var proejctAddr = testutils.TestAddress("proejctAddr") +// var projectRealm = std.NewUserRealm(proejctAddr) +var ( + projectAddr = testutils.TestAddress("projectAddr") + user01 = testutils.TestAddress("user01") + + projectRealm = std.NewUserRealm(projectAddr) + user01Realm = std.NewUserRealm(user01) +) + +func init() { + println("test_init") +} + +func TestCreateProject(t *testing.T) { + std.TestSetRealm(gsaRealm) + std.TestSetOrigCaller(gsa) + + obl.Approve(a2u(consts.LAUNCHPAD_ADDR), uint64(1_000_000_000)) + std.TestSkipHeights(1) + + projectId := CreateProject( + oblPath, + projectAddr, + uint64(1_000_000_000), // 1000000000 + "gno.land/r/onbloc/foo*PAD*gno.land/r/onbloc/bar", + "1*PAD*2", + uint64(10), // 100000000 + uint64(20), // 200000000 + uint64(70), // 700000000 + uint64(time.Now().Unix()+10), // 10s later + ) + shouldEQ(t, projectId, `gno.land/r/onbloc/obl:124`) + std.TestSkipHeights(1) + +} + +func TestDepositGnsToTier30(t *testing.T) { + std.TestSetRealm(gsaRealm) + std.TestSetOrigCaller(gsa) + gns.Transfer(a2u(user01), uint64(1_000_000)) // to deposit + // transfer some grc20 tokens to bypass project condition + foo.Transfer(a2u(user01), uint64(10)) + bar.Transfer(a2u(user01), uint64(10)) + + std.TestSetRealm(user01Realm) + std.TestSetOrigCaller(user01) + gns.Approve(a2u(consts.LAUNCHPAD_ADDR), uint64(1_000_000)) + + // skip some blocks to make project active + std.TestSkipHeights(4) + depositId := DepositGns("gno.land/r/onbloc/obl:124:30", uint64(1_000_000)) + shouldEQ(t, depositId, `gno.land/r/onbloc/obl:124:30:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) + std.TestSkipHeights(1) +} + +func TestCollectRewardByProjectTier(t *testing.T) { + std.TestSetRealm(user01Realm) + std.TestSetOrigCaller(user01) + + t.Run("claim reward before 30 days", func(t *testing.T) { + reward := CollectRewardByProjectTier(`gno.land/r/onbloc/obl:124:30`) + shouldEQ(t, reward, uint64(0)) + std.TestSkipHeights(1) + println() + }) + + t.Run("claim after 30 days", func(t *testing.T) { + std.TestSkipHeights(int64(TIMESTAMP_30DAYS) / 2) + reward := CollectRewardByProjectTier(`gno.land/r/onbloc/obl:124:30`) + shouldEQ(t, reward, uint64(99999999)) + println() + }) + + t.Run("no more claim in same block", func(t *testing.T) { + reward := CollectRewardByProjectTier(`gno.land/r/onbloc/obl:124:30`) + shouldEQ(t, reward, uint64(0)) + println() + }) + + // + shouldEQ(t, len(deposits), 1) + deposit := deposits[`gno.land/r/onbloc/obl:124:30:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`] + shouldEQ(t, deposit.id, `gno.land/r/onbloc/obl:124:30:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) + shouldEQ(t, deposit.projectId, `gno.land/r/onbloc/obl:124`) + shouldEQ(t, deposit.tier, `30`) + shouldEQ(t, deposit.depositor, user01) + shouldEQ(t, deposit.amount, uint64(1000000)) + shouldEQ(t, deposit.depositHeight, uint64(129)) + shouldEQ(t, deposit.depositTime, uint64(1234567902)) + shouldEQ(t, deposit.depositCollectHeight, uint64(0)) + shouldEQ(t, deposit.depositCollectTime, uint64(0)) + shouldEQ(t, deposit.rewardAmount, uint64(0)) + shouldEQ(t, deposit.rewardCollectHeight, uint64(1296131)) + shouldEQ(t, deposit.rewardCollectTime, uint64(1237159906)) + + // + shouldEQ(t, len(depositsByProject), 1) + shouldEQ(t, depositsByProject[`gno.land/r/onbloc/obl:124`][`30`][0], `gno.land/r/onbloc/obl:124:30:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) + + // + shouldEQ(t, len(depositsByUser), 1) + shouldEQ(t, depositsByUser[`g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv`][0], `gno.land/r/onbloc/obl:124:30:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) +} diff --git a/launchpad/tests/__TEST_launchpad_tier30_single_deposit_05_deposit_reward_by_proejct_tier_deposit_test.gnoA b/launchpad/tests/__TEST_launchpad_tier30_single_deposit_05_deposit_reward_by_proejct_tier_deposit_test.gnoA new file mode 100644 index 00000000..1cccd90b --- /dev/null +++ b/launchpad/tests/__TEST_launchpad_tier30_single_deposit_05_deposit_reward_by_proejct_tier_deposit_test.gnoA @@ -0,0 +1,118 @@ +package launchpad + +import ( + "std" + "testing" + "time" + + "gno.land/p/demo/testutils" + + "gno.land/r/gnoswap/v2/consts" + + "gno.land/r/onbloc/bar" + "gno.land/r/onbloc/foo" + "gno.land/r/onbloc/obl" + + "gno.land/r/gnoswap/v2/gns" +) + +// var proejctAddr = testutils.TestAddress("proejctAddr") +// var projectRealm = std.NewUserRealm(proejctAddr) +var ( + projectAddr = testutils.TestAddress("projectAddr") + user01 = testutils.TestAddress("user01") + + projectRealm = std.NewUserRealm(projectAddr) + user01Realm = std.NewUserRealm(user01) +) + +func init() { + println("test_init") +} + +func TestCreateProject(t *testing.T) { + std.TestSetRealm(gsaRealm) + std.TestSetOrigCaller(gsa) + + obl.Approve(a2u(consts.LAUNCHPAD_ADDR), uint64(1_000_000_000)) + std.TestSkipHeights(1) + + projectId := CreateProject( + oblPath, + projectAddr, + uint64(1_000_000_000), // 1000000000 + "gno.land/r/onbloc/foo*PAD*gno.land/r/onbloc/bar", + "1*PAD*2", + uint64(10), // 100000000 + uint64(20), // 200000000 + uint64(70), // 700000000 + uint64(time.Now().Unix()+10), // 10s later + ) + shouldEQ(t, projectId, `gno.land/r/onbloc/obl:124`) + std.TestSkipHeights(1) +} + +func TestDepositGnsToTier30(t *testing.T) { + std.TestSetRealm(gsaRealm) + std.TestSetOrigCaller(gsa) + gns.Transfer(a2u(user01), uint64(1_000_000)) // to deposit + // transfer some grc20 tokens to bypass project condition + foo.Transfer(a2u(user01), uint64(10)) + bar.Transfer(a2u(user01), uint64(10)) + + std.TestSetRealm(user01Realm) + std.TestSetOrigCaller(user01) + gns.Approve(a2u(consts.LAUNCHPAD_ADDR), uint64(1_000_000)) + + // skip some blocks to make project active + std.TestSkipHeights(4) + depositId := DepositGns("gno.land/r/onbloc/obl:124:30", uint64(1_000_000)) + shouldEQ(t, depositId, `gno.land/r/onbloc/obl:124:30:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) + std.TestSkipHeights(1) +} + +func TestCollectRewardByDepositId(t *testing.T) { + std.TestSetRealm(user01Realm) + std.TestSetOrigCaller(user01) + + t.Run("claim reward before 3 days(for 30day tier's init reward)", func(t *testing.T) { + reward := CollectRewardByDepositId(`gno.land/r/onbloc/obl:124:30:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) + shouldEQ(t, reward, uint64(0)) + std.TestSkipHeights(1) + println() + }) + + t.Run("claim after 3 days", func(t *testing.T) { + std.TestSkipHeights(int64(TIMESTAMP_3DAYS) / 2) + reward := CollectRewardByDepositId(`gno.land/r/onbloc/obl:124:30:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) + shouldEQ(t, reward, uint64(10000154)) + println() + }) + + t.Run("no more claim in same block", func(t *testing.T) { + reward := CollectRewardByDepositId(`gno.land/r/onbloc/obl:124:30:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) + shouldEQ(t, reward, uint64(0)) + println() + }) + + t.Run("wait 1 more block, then claim", func(t *testing.T) { + std.TestSkipHeights(1) + reward := CollectRewardByDepositId(`gno.land/r/onbloc/obl:124:30:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) + shouldEQ(t, reward, uint64(77)) + println() + }) + + t.Run("30day tier is over", func(t *testing.T) { + std.TestSkipHeights(int64(TIMESTAMP_30DAYS) / 2) + reward := CollectRewardByDepositId(`gno.land/r/onbloc/obl:124:30:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) + shouldEQ(t, reward, uint64(89999768)) + println() + }) + + t.Run("more block after 30 days", func(t *testing.T) { + std.TestSkipHeights(int64(TIMESTAMP_30DAYS) / 2) + reward := CollectRewardByDepositId(`gno.land/r/onbloc/obl:124:30:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) + shouldEQ(t, reward, uint64(0)) + println() + }) +} diff --git a/launchpad/tests/__TEST_launchpad_tier30_single_refund_project_deposit_test.gnoA b/launchpad/tests/__TEST_launchpad_tier30_single_refund_project_deposit_test.gnoA new file mode 100644 index 00000000..a87b79c3 --- /dev/null +++ b/launchpad/tests/__TEST_launchpad_tier30_single_refund_project_deposit_test.gnoA @@ -0,0 +1,154 @@ +package launchpad + +import ( + "std" + "testing" + "time" + + "gno.land/p/demo/testutils" + + "gno.land/r/gnoswap/v2/consts" + + "gno.land/r/onbloc/bar" + "gno.land/r/onbloc/foo" + "gno.land/r/onbloc/obl" + + "gno.land/r/gnoswap/v2/gns" +) + +var ( + projectAddr = testutils.TestAddress("projectAddr") + user01 = testutils.TestAddress("user01") + + projectRealm = std.NewUserRealm(projectAddr) + user01Realm = std.NewUserRealm(user01) +) + +func init() { + println("test_init") +} + +func TestCreateProject(t *testing.T) { + std.TestSetRealm(gsaRealm) + std.TestSetOrigCaller(gsa) + + obl.Approve(a2u(consts.LAUNCHPAD_ADDR), uint64(1_000_000_000)) + std.TestSkipHeights(1) + + projectId := CreateProject( + oblPath, + projectAddr, + uint64(1_000_000_000), // 1000000000 + "gno.land/r/onbloc/foo*PAD*gno.land/r/onbloc/bar", + "1*PAD*2", + uint64(10), // 100000000 + uint64(20), // 200000000 + uint64(70), // 700000000 + uint64(time.Now().Unix()+10), // 10s later + ) + shouldEQ(t, projectId, `gno.land/r/onbloc/obl:124`) + std.TestSkipHeights(10) // skip 10 block, make project active +} + +func TestDepositGnsToTier30(t *testing.T) { + std.TestSetRealm(gsaRealm) + std.TestSetOrigCaller(gsa) + gns.Transfer(a2u(user01), uint64(1_000_000)) // to deposit + // transfer some grc20 tokens to bypass project condition + foo.Transfer(a2u(user01), uint64(10)) + bar.Transfer(a2u(user01), uint64(10)) + + std.TestSetRealm(user01Realm) + std.TestSetOrigCaller(user01) + gns.Approve(a2u(consts.LAUNCHPAD_ADDR), uint64(1_000_000)) + + // skip some blocks to make project active + std.TestSkipHeights(4) + depositId := DepositGns("gno.land/r/onbloc/obl:124:30", uint64(1_000_000)) + shouldEQ(t, depositId, `gno.land/r/onbloc/obl:124:30:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:138`) + std.TestSkipHeights(1) +} + +func TestCollectRewardByProjectId(t *testing.T) { + std.TestSetRealm(user01Realm) + std.TestSetOrigCaller(user01) + + t.Run("collect reward before 30 days", func(t *testing.T) { + reward := CollectRewardByProjectId(`gno.land/r/onbloc/obl:124`) + shouldEQ(t, reward, uint64(0)) + std.TestSkipHeights(1) + println() + }) + + t.Run("collect after 30 days", func(t *testing.T) { + std.TestSkipHeights(int64(TIMESTAMP_30DAYS) / 2) + reward := CollectRewardByProjectId(`gno.land/r/onbloc/obl:124`) + shouldEQ(t, reward, uint64(99999305)) + println() + }) + + t.Run("no more collect in same block", func(t *testing.T) { + reward := CollectRewardByProjectId(`gno.land/r/onbloc/obl:124`) + shouldEQ(t, reward, uint64(0)) + println() + }) + + // + shouldEQ(t, len(deposits), 1) + deposit := deposits[`gno.land/r/onbloc/obl:124:30:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:138`] + shouldEQ(t, deposit.id, `gno.land/r/onbloc/obl:124:30:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:138`) + shouldEQ(t, deposit.projectId, `gno.land/r/onbloc/obl:124`) + shouldEQ(t, deposit.tier, `30`) + shouldEQ(t, deposit.depositor, user01) + shouldEQ(t, deposit.amount, uint64(1000000)) + shouldEQ(t, deposit.depositHeight, uint64(138)) + shouldEQ(t, deposit.depositTime, uint64(1234567920)) + shouldEQ(t, deposit.depositCollectHeight, uint64(0)) + shouldEQ(t, deposit.depositCollectTime, uint64(0)) + shouldEQ(t, deposit.rewardAmount, uint64(0)) + shouldEQ(t, deposit.rewardCollectHeight, uint64(1296140)) + shouldEQ(t, deposit.rewardCollectTime, uint64(1237159924)) + + // + shouldEQ(t, len(depositsByProject), 1) + shouldEQ(t, depositsByProject[`gno.land/r/onbloc/obl:124`][`30`][0], `gno.land/r/onbloc/obl:124:30:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:138`) + + // + shouldEQ(t, len(depositsByUser), 1) + shouldEQ(t, depositsByUser[`g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv`][0], `gno.land/r/onbloc/obl:124:30:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:138`) +} + +func TestRefundProject(t *testing.T) { + t.Run("unauthorized", func(t *testing.T) { + std.TestSetRealm(user01Realm) + std.TestSetOrigCaller(user01) + shouldPanicWithMsg( + t, + func() { + RefundProject(`gno.land/r/onbloc/obl:124`) + }, + `only recipient can refund`, + ) + }) + + t.Run("not ended", func(t *testing.T) { + std.TestSetRealm(projectRealm) + std.TestSetOrigCaller(projectAddr) + shouldPanicWithMsg( + t, + func() { + RefundProject(`gno.land/r/onbloc/obl:124`) + }, + `project not ended yet`, + ) + }) + + t.Run("ended with refund", func(t *testing.T) { + std.TestSetRealm(projectRealm) + std.TestSetOrigCaller(projectAddr) + std.TestSkipHeights(int64(TIMESTAMP_180DAYS) / 2) + refund := RefundProject(`gno.land/r/onbloc/obl:124`) + shouldNEQ(t, refund, uint64(0)) + std.TestSkipHeights(1) + }) +} diff --git a/launchpad/tests/__TEST_launchpad_tier30_two_deposit_reward_by_project_test.gnoA b/launchpad/tests/__TEST_launchpad_tier30_two_deposit_reward_by_project_test.gnoA new file mode 100644 index 00000000..3c56933a --- /dev/null +++ b/launchpad/tests/__TEST_launchpad_tier30_two_deposit_reward_by_project_test.gnoA @@ -0,0 +1,143 @@ +package launchpad + +import ( + "std" + "testing" + "time" + + "gno.land/p/demo/testutils" + + "gno.land/r/gnoswap/v2/consts" + + "gno.land/r/onbloc/bar" + "gno.land/r/onbloc/foo" + "gno.land/r/onbloc/obl" + + "gno.land/r/gnoswap/v2/gns" +) + +var ( + projectAddr = testutils.TestAddress("projectAddr") + user01 = testutils.TestAddress("user01") + user02 = testutils.TestAddress("user02") + + projectRealm = std.NewUserRealm(projectAddr) + user01Realm = std.NewUserRealm(user01) + user02Realm = std.NewUserRealm(user02) +) + +func init() { + println("test_init") +} + +func TestCreateProject(t *testing.T) { + std.TestSetRealm(gsaRealm) + std.TestSetOrigCaller(gsa) + + obl.Approve(a2u(consts.LAUNCHPAD_ADDR), uint64(1_000_000_000)) + std.TestSkipHeights(1) + + projectId := CreateProject( + oblPath, + projectAddr, + uint64(1_000_000_000), // 1000000000 + "gno.land/r/onbloc/foo*PAD*gno.land/r/onbloc/bar", + "1*PAD*2", + uint64(10), // 100000000 + uint64(20), // 200000000 + uint64(70), // 700000000 + uint64(time.Now().Unix()+10), // 10s later + ) + shouldEQ(t, projectId, `gno.land/r/onbloc/obl:124`) + std.TestSkipHeights(1) + +} + +func TestDepositGnsToTier30_User01(t *testing.T) { + std.TestSetRealm(gsaRealm) + std.TestSetOrigCaller(gsa) + gns.Transfer(a2u(user01), uint64(1_000_000)) // to deposit + // transfer some grc20 tokens to bypass project condition + foo.Transfer(a2u(user01), uint64(10)) + bar.Transfer(a2u(user01), uint64(10)) + + std.TestSetRealm(user01Realm) + std.TestSetOrigCaller(user01) + gns.Approve(a2u(consts.LAUNCHPAD_ADDR), uint64(1_000_000)) + + // skip some blocks to make project active + std.TestSkipHeights(4) + DepositGns("gno.land/r/onbloc/obl:124:30", uint64(1_000_000)) + std.TestSkipHeights(1) +} + +func TestDepositGnsToTier30_User02(t *testing.T) { + std.TestSetRealm(gsaRealm) + std.TestSetOrigCaller(gsa) + gns.Transfer(a2u(user02), uint64(9_000_000)) // to deposit + // transfer some grc20 tokens to bypass project condition + foo.Transfer(a2u(user02), uint64(10)) + bar.Transfer(a2u(user02), uint64(10)) + + std.TestSetRealm(user02Realm) + std.TestSetOrigCaller(user02) + gns.Approve(a2u(consts.LAUNCHPAD_ADDR), uint64(9_000_000)) + + // skip some blocks to make project active + DepositGns("gno.land/r/onbloc/obl:124:30", uint64(1_000_000)) + std.TestSkipHeights(1) +} + +func TestCollectRewardByProjectId(t *testing.T) { + t.Run("claim reward before 30 days by user01", func(t *testing.T) { + std.TestSetRealm(user01Realm) + std.TestSetOrigCaller(user01) + + reward := CollectRewardByProjectId(`gno.land/r/onbloc/obl:124`) + shouldEQ(t, reward, uint64(0)) + std.TestSkipHeights(1) + }) + + t.Run("claim reward before 30 days by user02", func(t *testing.T) { + std.TestSetRealm(user02Realm) + std.TestSetOrigCaller(user02) + + reward := CollectRewardByProjectId(`gno.land/r/onbloc/obl:124`) + shouldEQ(t, reward, uint64(0)) + std.TestSkipHeights(1) + }) + + t.Run("claim after 30 days by user01", func(t *testing.T) { + std.TestSetRealm(user01Realm) + std.TestSetOrigCaller(user01) + + std.TestSkipHeights(int64(TIMESTAMP_30DAYS) / 2) + reward := CollectRewardByProjectId(`gno.land/r/onbloc/obl:124`) + shouldEQ(t, reward, uint64(50000037)) + }) + + t.Run("claim after 30 days by user02", func(t *testing.T) { + std.TestSetRealm(user02Realm) + std.TestSetOrigCaller(user02) + + std.TestSkipHeights(int64(TIMESTAMP_30DAYS) / 2) + reward := CollectRewardByProjectId(`gno.land/r/onbloc/obl:124`) + shouldEQ(t, reward, uint64(49999960)) + }) + + t.Run("no more claim in same block by user 01", func(t *testing.T) { + std.TestSetRealm(user01Realm) + std.TestSetOrigCaller(user01) + + reward := CollectRewardByProjectId(`gno.land/r/onbloc/obl:124`) + shouldEQ(t, reward, uint64(0)) + }) + + t.Run("no more claim in same block by user 02", func(t *testing.T) { + std.TestSetRealm(user02Realm) + std.TestSetOrigCaller(user02) + + reward := CollectRewardByProjectId(`gno.land/r/onbloc/obl:124`) + shouldEQ(t, reward, uint64(0)) + }) +} diff --git a/launchpad/tests/__TEST_launchpad_tier90_single_deposit_reward_by_proejct_tier_test.gnoA b/launchpad/tests/__TEST_launchpad_tier90_single_deposit_reward_by_proejct_tier_test.gnoA new file mode 100644 index 00000000..ffa245b4 --- /dev/null +++ b/launchpad/tests/__TEST_launchpad_tier90_single_deposit_reward_by_proejct_tier_test.gnoA @@ -0,0 +1,124 @@ +package launchpad + +import ( + "std" + "testing" + "time" + + "gno.land/p/demo/testutils" + + "gno.land/r/gnoswap/v2/consts" + + "gno.land/r/onbloc/bar" + "gno.land/r/onbloc/foo" + "gno.land/r/onbloc/obl" + + "gno.land/r/gnoswap/v2/gns" +) + +var ( + projectAddr = testutils.TestAddress("projectAddr") + user01 = testutils.TestAddress("user01") + + projectRealm = std.NewUserRealm(projectAddr) + user01Realm = std.NewUserRealm(user01) +) + +func init() { + println("test_init") +} + +func TestCreateProject(t *testing.T) { + std.TestSetRealm(gsaRealm) + std.TestSetOrigCaller(gsa) + + obl.Approve(a2u(consts.LAUNCHPAD_ADDR), uint64(1_000_000_000)) + std.TestSkipHeights(1) + + projectId := CreateProject( + oblPath, + projectAddr, + uint64(1_000_000_000), // 1000000000 + "gno.land/r/onbloc/foo*PAD*gno.land/r/onbloc/bar", + "1*PAD*2", + uint64(10), // 100000000 + uint64(20), // 200000000 + uint64(70), // 700000000 + uint64(time.Now().Unix()+10), // 10s later + ) + shouldEQ(t, projectId, `gno.land/r/onbloc/obl:124`) + std.TestSkipHeights(1) +} + +func TestDepositGnsToTier90(t *testing.T) { + std.TestSetRealm(gsaRealm) + std.TestSetOrigCaller(gsa) + gns.Transfer(a2u(user01), uint64(1_000_000)) // to deposit + // transfer some grc20 tokens to bypass project condition + foo.Transfer(a2u(user01), uint64(10)) + bar.Transfer(a2u(user01), uint64(10)) + + std.TestSetRealm(user01Realm) + std.TestSetOrigCaller(user01) + gns.Approve(a2u(consts.LAUNCHPAD_ADDR), uint64(1_000_000)) + + // skip some blocks to make project active + std.TestSkipHeights(4) + depositId := DepositGns("gno.land/r/onbloc/obl:124:90", uint64(1_000_000)) + shouldEQ(t, depositId, `gno.land/r/onbloc/obl:124:90:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) + std.TestSkipHeights(1) +} + +func TestCollectRewardByDepositId(t *testing.T) { + std.TestSetRealm(user01Realm) + std.TestSetOrigCaller(user01) + + t.Run("claim reward before 7 days(for 90day tier's init reward)", func(t *testing.T) { + reward := CollectRewardByDepositId(`gno.land/r/onbloc/obl:124:90:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) + shouldEQ(t, reward, uint64(0)) + std.TestSkipHeights(1) + println() + }) + + t.Run("claim reward before 7 days(for 90day tier's init reward)", func(t *testing.T) { + std.TestSkipHeights(123) + reward := CollectRewardByDepositId(`gno.land/r/onbloc/obl:124:90:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) + shouldEQ(t, reward, uint64(0)) + std.TestSkipHeights(1) + println() + }) + + t.Run("claim after 7 days", func(t *testing.T) { + std.TestSkipHeights(int64(TIMESTAMP_7DAYS) / 2) + reward := CollectRewardByDepositId(`gno.land/r/onbloc/obl:124:90:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) + shouldEQ(t, reward, uint64(15562035)) + println() + }) + + t.Run("no more claim in same block", func(t *testing.T) { + reward := CollectRewardByDepositId(`gno.land/r/onbloc/obl:124:90:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) + shouldEQ(t, reward, uint64(0)) + println() + }) + + t.Run("wait 1 more block, then claim", func(t *testing.T) { + std.TestSkipHeights(1) + reward := CollectRewardByDepositId(`gno.land/r/onbloc/obl:124:90:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) + shouldEQ(t, reward, uint64(51)) + println() + }) + + t.Run("30day tier is over", func(t *testing.T) { + std.TestSkipHeights(int64(TIMESTAMP_90DAYS) / 2) + reward := CollectRewardByDepositId(`gno.land/r/onbloc/obl:124:90:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) + shouldEQ(t, reward, uint64(184437911)) + println() + }) + + t.Run("more block after 30 days", func(t *testing.T) { + std.TestSkipHeights(int64(TIMESTAMP_30DAYS) / 2) + reward := CollectRewardByDepositId(`gno.land/r/onbloc/obl:124:90:g1w4ek2u3sx9047h6lta047h6lta047h6lh0ssfv:129`) + shouldEQ(t, reward, uint64(0)) + println() + }) +} diff --git a/launchpad/token_register.gno b/launchpad/token_register.gno new file mode 100644 index 00000000..42e90710 --- /dev/null +++ b/launchpad/token_register.gno @@ -0,0 +1,150 @@ +package launchpad + +import ( + "std" + + "gno.land/p/demo/ufmt" + + pusers "gno.land/p/demo/users" + + "gno.land/r/gnoswap/v2/consts" +) + +// GRC20Interface is the interface for GRC20 tokens +// It is used to interact with the GRC20 tokens without importing but by registering each tokens function +type GRC20Interface interface { + Transfer() func(to pusers.AddressOrName, amount uint64) + TransferFrom() func(from, to pusers.AddressOrName, amount uint64) + BalanceOf() func(owner pusers.AddressOrName) uint64 + Approve() func(spender pusers.AddressOrName, amount uint64) +} + +var ( + registered = make(map[string]GRC20Interface) + locked = false // mutex +) + +// GetRegisteredTokens returns a list of all registered tokens +func GetRegisteredTokens() []string { + tokens := make([]string, 0, len(registered)) + for k := range registered { + tokens = append(tokens, k) + } + return tokens +} + +// RegisterGRC20Interface registers a GRC20 token interface +// +// Panics: +// - caller is not the admin +// - token already registered +func RegisterGRC20Interface(pkgPath string, igrc20 GRC20Interface) { + caller := std.GetOrigCaller() + if caller != consts.TOKEN_REGISTER { + panic(ufmt.Sprintf("[LAUNCH_PAD] token_register.gno__RegisterGRC20Interface() || unauthorized address(%s) to register", caller.String())) + } + + pkgPath = handleNative(pkgPath) + + _, found := registered[pkgPath] + if found { + panic(ufmt.Sprintf("[LAUNCH_PAD] token_register.gno__RegisterGRC20Interface() || pkgPath(%s) already registered", pkgPath)) + } + + registered[pkgPath] = igrc20 +} + +// UnregisterGRC20Interface unregisters a GRC20 token interface +// +// Panics: +// - caller is not the admin +func UnregisterGRC20Interface(pkgPath string) { + // only admin can unregister + caller := std.GetOrigCaller() + if caller != consts.TOKEN_REGISTER { + panic(ufmt.Sprintf("[LAUNCH_PAD] token_register.gno__UnregisterGRC20Interface() || unauthorized address(%s) to unregister", caller.String())) + } + + pkgPath = handleNative(pkgPath) + + _, found := registered[pkgPath] + if found { + delete(registered, pkgPath) + } +} + +func transferByRegisterCall(pkgPath string, to std.Address, amount uint64) bool { + pkgPath = handleNative(pkgPath) + + _, found := registered[pkgPath] + if !found { + panic(ufmt.Sprintf("[LAUNCH_PAD] token_register.gno__transferByRegisterCall() || pkgPath(%s) not found", pkgPath)) + } + + if !locked { + locked = true + registered[pkgPath].Transfer()(pusers.AddressOrName(to), amount) + + defer func() { + locked = false + }() + } else { + panic("[LAUNCH_PAD] token_register.gno__transferByRegisterCall() || expected locked to be false") + } + + return true +} + +func transferFromByRegisterCall(pkgPath string, from, to std.Address, amount uint64) bool { + pkgPath = handleNative(pkgPath) + + _, found := registered[pkgPath] + if !found { + panic(ufmt.Sprintf("[LAUNCH_PAD] token_register.gno__transferFromByRegisterCall() || pkgPath(%s) not found", pkgPath)) + } + + if !locked { + locked = true + registered[pkgPath].TransferFrom()(pusers.AddressOrName(from), pusers.AddressOrName(to), amount) + + defer func() { + locked = false + }() + } else { + panic("[LAUNCH_PAD] token_register.gno__transferFromByRegisterCall() || expected locked to be false") + } + return true +} + +func balanceOfByRegisterCall(pkgPath string, owner std.Address) uint64 { + pkgPath = handleNative(pkgPath) + + _, found := registered[pkgPath] + if !found { + panic(ufmt.Sprintf("[LAUNCH_PAD] token_register.gno__balanceOfByRegisterCall() || pkgPath(%s) not found", pkgPath)) + } + + balance := registered[pkgPath].BalanceOf()(pusers.AddressOrName(owner)) + return balance +} + +func approveByRegisterCall(pkgPath string, spender std.Address, amount uint64) bool { + pkgPath = handleNative(pkgPath) + + _, found := registered[pkgPath] + if !found { + panic(ufmt.Sprintf("[LAUNCH_PAD] token_register.gno__approveByRegisterCall() || pkgPath(%s) not found", pkgPath)) + } + + registered[pkgPath].Approve()(pusers.AddressOrName(spender), amount) + + return true +} + +func handleNative(pkgPath string) string { + if pkgPath == consts.GNOT { + return consts.WRAPPED_WUGNOT + } + + return pkgPath +} diff --git a/launchpad/type.gno b/launchpad/type.gno new file mode 100644 index 00000000..2567e34d --- /dev/null +++ b/launchpad/type.gno @@ -0,0 +1,84 @@ +package launchpad + +import ( + "std" + + u256 "gno.land/p/gnoswap/uint256" +) + +type Project struct { + id string // 'tokenPath:createdHeight' + tokenPath string + depositAmount uint64 + recipient std.Address // string + + conditions map[string]Condition // tokenPath -> Condition + + tier30Ratio uint64 + tier30 Tier + + tier90 Tier + tier90Ratio uint64 + + tier180 Tier + tier180Ratio uint64 + + createdHeight uint64 + createdTime uint64 + + startHeight uint64 + startTime uint64 + endHeight uint64 // same with tier 180's data + endTime uint64 // same with tier 180's data + + totalDepositAmount uint64 // won't be decreased + actualDepositAmount uint64 // will be decreased if deposit collected 'CollectDepositGns()' + + totalParticipant uint64 // accu, won't be decreased + actualParticipant uint64 // will be decreased if deposit collected 'CollectDepositGns()' + + totalCollectedAmount uint64 // collect reward amount +} + +type Tier struct { + id string // '{projectId}:duration' // duartion == 30, 90, 180 + collectWaitDuration uint64 + tierAmount uint64 + tierAmountPerBlockX96 *u256.Uint + + // start height/time is same as the project + endHeight uint64 + endTime uint64 + + // actual data + // unlikely projects' totalDepositAmount or totalParticipant + // below data will be decreased + depositAmount uint64 + participant uint64 +} + +type Condition struct { + tokenPath string + minAmount uint64 +} + +type Deposit struct { + id string // 'projectId:tier:depositor:height' + + projectId string // + tier string // 30, 60, 180 // instead of tierId + depositor std.Address // string + amount uint64 + + depositHeight uint64 + depositTime uint64 + + // deposit + depositCollectHeight uint64 + depositCollectTime uint64 + + // reward collect + rewardAmount uint64 + rewardCollectHeight uint64 + rewardCollectTime uint64 +} diff --git a/launchpad/util.gno b/launchpad/util.gno new file mode 100644 index 00000000..1a3a1ef6 --- /dev/null +++ b/launchpad/util.gno @@ -0,0 +1,228 @@ +package launchpad + +import ( + b64 "encoding/base64" + "std" + "strconv" + + u256 "gno.land/p/gnoswap/uint256" + + "gno.land/p/demo/json" + "gno.land/p/demo/ufmt" + + pusers "gno.land/p/demo/users" +) + +const ( + MaxUint64 = 1<<64 - 1 + uintSize = 32 << (^uint(0) >> 63) +) + +func lower(c byte) byte { + return c | ('x' - 'X') +} + +// TODO: Remove parseUint after gno supports strconv.ParseUint +func parseUint(s string, base int, bitSize int) uint64 { + const fnParseUint = "ParseUint" + + if s == "" { + panic(ufmt.Errorf("%s: parsing \"\": invalid syntax", fnParseUint)) + } + + base0 := base == 0 + + s0 := s + switch { + case 2 <= base && base <= 36: + // valid base; nothing to do + + case base == 0: + // Look for octal, hex prefix. + base = 10 + if s[0] == '0' { + switch { + case len(s) >= 3 && lower(s[1]) == 'b': + base = 2 + s = s[2:] + case len(s) >= 3 && lower(s[1]) == 'o': + base = 8 + s = s[2:] + case len(s) >= 3 && lower(s[1]) == 'x': + base = 16 + s = s[2:] + default: + base = 8 + s = s[1:] + } + } + + default: + panic(ufmt.Errorf("%s: invalid base %d", fnParseUint, base)) + } + + if bitSize == 0 { + bitSize = uintSize + } else if bitSize < 0 || bitSize > 64 { + panic(ufmt.Errorf("%s: invalid bit size %d", fnParseUint, bitSize)) + } + + // Cutoff is the smallest number such that cutoff*base > maxUint64. + // Use compile-time constants for common cases. + var cutoff uint64 + switch base { + case 10: + cutoff = MaxUint64/10 + 1 + case 16: + cutoff = MaxUint64/16 + 1 + default: + cutoff = MaxUint64/uint64(base) + 1 + } + + maxVal := uint64(1)<= byte(base) { + panic(ufmt.Errorf("%s: invalid character", fnParseUint)) + } + + if n >= cutoff { + // n*base overflows + panic(ufmt.Errorf("%s: value out of range", fnParseUint)) + // return maxVal, ufmt.Errorf("%s: value out of range", fnParseUint) + } + n *= uint64(base) + + n1 := n + uint64(d) + if n1 < n || n1 > maxVal { + // n+d overflows + panic(ufmt.Errorf("%s: value out of range", fnParseUint)) + // return maxVal, ufmt.Errorf("%s: value out of range", fnParseUint) + } + n = n1 + } + + if underscores && !underscoreOK(s0) { + panic(ufmt.Errorf("%s: invalid underscore", fnParseUint)) + } + + return n +} + +func underscoreOK(s string) bool { + // saw tracks the last character (class) we saw: + // ^ for beginning of number, + // 0 for a digit or base prefix, + // _ for an underscore, + // ! for none of the above. + saw := '^' + i := 0 + + // Optional sign. + if len(s) >= 1 && (s[0] == '-' || s[0] == '+') { + s = s[1:] + } + + // Optional base prefix. + hex := false + if len(s) >= 2 && s[0] == '0' && (lower(s[1]) == 'b' || lower(s[1]) == 'o' || lower(s[1]) == 'x') { + i = 2 + saw = '0' // base prefix counts as a digit for "underscore as digit separator" + hex = lower(s[1]) == 'x' + } + + // Number proper. + for ; i < len(s); i++ { + // Digits are always okay. + if '0' <= s[i] && s[i] <= '9' || hex && 'a' <= lower(s[i]) && lower(s[i]) <= 'f' { + saw = '0' + continue + } + // Underscore must follow digit. + if s[i] == '_' { + if saw != '0' { + return false + } + saw = '_' + continue + } + // Underscore must also be followed by digit. + if saw == '_' { + return false + } + // Saw non-digit, non-underscore. + saw = '!' + } + + return saw != '_' +} + +func strToInt(str string) int { + res, err := strconv.Atoi(str) + if err != nil { + panic(err) + } + + return res +} + +func strToU256U64(str string) uint64 { + strValue := u256.MustFromDecimal(str) + return strValue.Uint64() +} + +func contains(slice []string, str string) bool { + for _, v := range slice { + if v == str { + return true + } + } + return false +} + +func marshal(data *json.Node) string { + b, err := json.Marshal(data) + if err != nil { + panic(err.Error()) + } + + return string(b) +} + +func b64Encode(data string) string { + return string(b64.StdEncoding.EncodeToString([]byte(data))) +} + +func origCaller() string { + return std.GetOrigCaller().String() +} + +func prevRealm() string { + return std.PrevRealm().PkgPath() +} + +func a2u(addr std.Address) pusers.AddressOrName { + return pusers.AddressOrName(addr) +} + +func minU64(x, y uint64) uint64 { + if x < y { + return x + } + return y +}