package crowdfund import ( "strings" "testing" "chain" "gno.land/p/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/coinio" "gno.land/p/nt/testutils/v0" "gno.land/p/nt/uassert/v0" ) var ( creator1 = testutils.TestAddress("cfcreator1") creator2 = testutils.TestAddress("cfcreator2") backerA = testutils.TestAddress("cfbackera") backerB = testutils.TestAddress("cfbackerb") mallory = testutils.TestAddress("cfmallory") ) // HARNESS NOTES, carried from the treasury_board, vesting and grants // suites (measured there, relied on here): // // - testing.SetRealm records the override per FRAME INDEX: stage the // caller INLINE in the test function's frame before a uassert call, // or inside a helper that stages and calls in one frame of its own. // Never inside a uassert closure — that staging does not take. // // - testing.SkipHeights REPLACES the whole context (OriginCaller, // CurrentRealm, OriginSend included). Re-stage after every skip. // // - Realm globals PERSIST across tests while banker state resets per // test. begin() disarms any leftover envelope and re-seeds the // realm's bank to its carried liabilities, so conservation is // asserted in DELTA form. // // - A panic caught by uassert does NOT roll back realm globals here, // whereas on-chain the whole transaction reverts. Every abort // asserted below happens strictly BEFORE this realm writes state, // with one measured exception documented inline // (TestClaimTimingIsTheDuebooksVerdict). // // - The SEND envelope is process-global: a staged envelope must be // cleared (SetOriginSend(nil)) before the next non-payable call, or // rejectStraySend fires and the test asserts the harness leak, not // the guard. func init() { self = chain.PackageAddress("gno.land/r/g1ut6uspuh73e02yauxpmyt8g3wwddaq8utagvm3/crowdfund") } type baseline struct { held int64 backed int64 fees int64 } func begin() baseline { testing.SetOriginSend(nil) if l := Liabilities(); l > 0 { testing.IssueCoins(Address(), ugnot(l)) } return baseline{held: Held(), backed: TotalBacked(), fees: FeesAccrued()} } func ugnot(n int64) chain.Coins { return chain.NewCoins(chain.NewCoin(Denom, n)) } // launchAs stages `from` and launches a MinGoal x MinDuration campaign. func launchAs(cur realm, from address, title string) int64 { testing.SetRealm(testing.NewUserRealm(from)) return Launch(cross(cur), title, "", MinGoal, MinDurationBlocks) } // pledgeAs stages a direct EOA call with -send and mirrors the // envelope into the realm's bank, as the chain would. func pledgeAs(cur realm, from address, id, amount int64) { testing.SetRealm(testing.NewUserRealm(from)) testing.SetOriginSend(ugnot(amount)) testing.IssueCoins(Address(), ugnot(amount)) Pledge(cross(cur), id) testing.SetOriginSend(nil) } func balanceOf(addr address) int64 { return coinio.HeldAt(addr, Denom) } // --- launch --- func TestLaunchBasics(cur realm, t *testing.T) { begin() before := NumCampaigns() openBefore := OpenCampaigns() id := launchAs(cur, creator1, "Solar panels for the hackerspace") uassert.Equal(t, before+1, id) uassert.Equal(t, before+1, NumCampaigns()) uassert.Equal(t, openBefore+1, OpenCampaigns()) uassert.Equal(t, int64(1), OpenOf(creator1)) cr, goal, raised, feeBps, createdAt, deadline, claimEnd, backers, state := CampaignInfo(id) uassert.Equal(t, creator1, cr) uassert.Equal(t, MinGoal, goal) uassert.Equal(t, int64(0), raised) uassert.Equal(t, SuccessFeeBps, feeBps) uassert.Equal(t, createdAt+MinDurationBlocks, deadline) uassert.Equal(t, deadline+ClaimWindowBlocks, claimEnd) uassert.Equal(t, 0, backers) uassert.Equal(t, stateFunding, state) uassert.Equal(t, stateFunding, Status(id)) // Clean up the open slot so later per-creator-cap tests see a known // count. testing.SetRealm(testing.NewUserRealm(creator1)) CreatorCancel(cross(cur), id) uassert.Equal(t, int64(0), OpenOf(creator1)) } func TestLaunchValidation(cur realm, t *testing.T) { begin() testing.SetRealm(testing.NewUserRealm(creator1)) uassert.AbortsWithMessage(t, cur, "title must not be empty", func() { Launch(cross(cur), "", "", MinGoal, MinDurationBlocks) }) testing.SetRealm(testing.NewUserRealm(creator1)) uassert.AbortsWithMessage(t, cur, "title exceeds 100 bytes", func() { Launch(cross(cur), strings.Repeat("x", MaxTitleLen+1), "", MinGoal, MinDurationBlocks) }) testing.SetRealm(testing.NewUserRealm(creator1)) uassert.AbortsWithMessage(t, cur, "memo exceeds 256 bytes", func() { Launch(cross(cur), "t", strings.Repeat("x", MaxMemoLen+1), MinGoal, MinDurationBlocks) }) testing.SetRealm(testing.NewUserRealm(creator1)) uassert.AbortsWithMessage(t, cur, "goal must be at least 1000000ugnot", func() { Launch(cross(cur), "t", "", MinGoal-1, MinDurationBlocks) }) // Duration bounds are the duebook's own validation, not a check in // this realm — asserted against the primitive's error text. testing.SetRealm(testing.NewUserRealm(creator1)) uassert.AbortsWithMessage(t, cur, "duebook: delay outside [minDelay, maxDelay]", func() { Launch(cross(cur), "t", "", MinGoal, MinDurationBlocks-1) }) testing.SetRealm(testing.NewUserRealm(creator1)) uassert.AbortsWithMessage(t, cur, "duebook: delay outside [minDelay, maxDelay]", func() { Launch(cross(cur), "t", "", MinGoal, MaxDurationBlocks+1) }) uassert.Equal(t, int64(0), OpenOf(creator1)) } func TestLaunchPerCreatorCap(cur realm, t *testing.T) { begin() uassert.Equal(t, int64(0), OpenOf(creator2)) ids := []int64{} for i := int64(0); i < MaxOpenPerCreator; i++ { ids = append(ids, launchAs(cur, creator2, "cap filler")) } uassert.Equal(t, MaxOpenPerCreator, OpenOf(creator2)) testing.SetRealm(testing.NewUserRealm(creator2)) uassert.AbortsWithMessage(t, cur, "creator already has 8 unsettled campaigns", func() { Launch(cross(cur), "one too many", "", MinGoal, MinDurationBlocks) }) // A settled campaign releases its slot. testing.SetRealm(testing.NewUserRealm(creator2)) CreatorCancel(cross(cur), ids[0]) uassert.Equal(t, MaxOpenPerCreator-1, OpenOf(creator2)) idNew := launchAs(cur, creator2, "slot reopened") uassert.Equal(t, MaxOpenPerCreator, OpenOf(creator2)) // Drain the slots so later tests start from a clean count. for _, id := range append(ids[1:], idNew) { testing.SetRealm(testing.NewUserRealm(creator2)) CreatorCancel(cross(cur), id) } uassert.Equal(t, int64(0), OpenOf(creator2)) } // --- pledging --- func TestPledgeAndConservation(cur realm, t *testing.T) { b := begin() id := launchAs(cur, creator1, "conservation case") pledgeAs(cur, backerA, id, 400_000) pledgeAs(cur, backerB, id, 250_000) pledgeAs(cur, backerA, id, 100_000) // same backer accumulates uassert.Equal(t, int64(500_000), PledgeOf(id, backerA)) uassert.Equal(t, int64(250_000), PledgeOf(id, backerB)) _, _, raised, _, _, _, _, backers, _ := CampaignInfo(id) uassert.Equal(t, int64(750_000), raised) uassert.Equal(t, 2, backers) // Conservation, delta form: every pledged coin is in B and in H. uassert.Equal(t, b.backed+750_000, TotalBacked()) uassert.Equal(t, b.held+750_000, Held()) uassert.Equal(t, b.fees, FeesAccrued()) uassert.True(t, Held() >= Liabilities()) // Clean up: back out both pledges, cancel. testing.SetRealm(testing.NewUserRealm(backerA)) Unpledge(cross(cur), id) testing.SetRealm(testing.NewUserRealm(backerB)) Unpledge(cross(cur), id) uassert.Equal(t, b.backed, TotalBacked()) testing.SetRealm(testing.NewUserRealm(creator1)) CreatorCancel(cross(cur), id) } func TestPledgeGuards(cur realm, t *testing.T) { begin() id := launchAs(cur, creator1, "guard case") // The payment-guard regression: a realm-routed pledge is refused by // coinio's receipt shape, not by anything this file could forget. testing.SetRealm(testing.NewCodeRealm("gno.land/r/demo/attacker")) testing.SetOriginSend(ugnot(1000)) uassert.AbortsWithMessage(t, cur, "coinio: payment must be a direct EOA call with -send (realms and maketx-run are rejected)", func() { Pledge(cross(cur), id) }) testing.SetOriginSend(nil) // Wrong denomination. testing.SetRealm(testing.NewUserRealm(backerA)) testing.SetOriginSend(chain.NewCoins(chain.NewCoin("foocoin", 1000))) uassert.AbortsWithMessage(t, cur, "coinio: send exactly one coin type: ugnot", func() { Pledge(cross(cur), id) }) testing.SetOriginSend(nil) // No envelope at all. testing.SetRealm(testing.NewUserRealm(backerA)) uassert.AbortsWithMessage(t, cur, "coinio: send exactly one coin type: ugnot", func() { Pledge(cross(cur), id) }) // Unknown campaign. testing.SetRealm(testing.NewUserRealm(backerA)) testing.SetOriginSend(ugnot(1000)) uassert.AbortsWithMessage(t, cur, "unknown campaign id", func() { Pledge(cross(cur), 999_999) }) testing.SetOriginSend(nil) // Below the pledge minimum (audit open question 1: dust-entry // bloat); exactly the minimum is accepted. testing.SetRealm(testing.NewUserRealm(backerA)) testing.SetOriginSend(ugnot(MinPledge - 1)) testing.IssueCoins(Address(), ugnot(MinPledge-1)) uassert.AbortsWithMessage(t, cur, "pledge at least 10000ugnot", func() { Pledge(cross(cur), id) }) testing.SetOriginSend(nil) pledgeAs(cur, backerA, id, MinPledge) uassert.Equal(t, MinPledge, PledgeOf(id, backerA)) testing.SetRealm(testing.NewUserRealm(backerA)) Unpledge(cross(cur), id) // Settled campaign. testing.SetRealm(testing.NewUserRealm(creator1)) CreatorCancel(cross(cur), id) testing.SetRealm(testing.NewUserRealm(backerA)) testing.SetOriginSend(ugnot(1000)) uassert.AbortsWithMessage(t, cur, "campaign is settled", func() { Pledge(cross(cur), id) }) testing.SetOriginSend(nil) } func TestPledgeClosesAtDeadline(cur realm, t *testing.T) { begin() id := launchAs(cur, creator1, "deadline case") testing.SkipHeights(MinDurationBlocks) // height == deadline exactly testing.SetRealm(testing.NewUserRealm(backerA)) testing.SetOriginSend(ugnot(1000)) testing.IssueCoins(Address(), ugnot(1000)) uassert.AbortsWithMessage(t, cur, "funding is closed", func() { Pledge(cross(cur), id) }) testing.SetOriginSend(nil) // Unpledge is likewise closed at the deadline (the failed path is // Refund's). testing.SetRealm(testing.NewUserRealm(backerA)) uassert.AbortsWithMessage(t, cur, "funding is closed; use Refund if the campaign failed", func() { Unpledge(cross(cur), id) }) // Failed (0 < goal) and past deadline: refunds open, nothing to pay. uassert.True(t, IsRefundable(id)) uassert.Equal(t, "failed (refunds open)", Status(id)) testing.SetRealm(testing.NewUserRealm(creator1)) CreatorCancel(cross(cur), id) } // --- unpledge --- func TestUnpledgeReturnsWholePledge(cur realm, t *testing.T) { b := begin() id := launchAs(cur, creator1, "unpledge case") pledgeAs(cur, backerA, id, 300_000) walletBefore := balanceOf(backerA) testing.SetRealm(testing.NewUserRealm(backerA)) Unpledge(cross(cur), id) uassert.Equal(t, walletBefore+300_000, balanceOf(backerA)) uassert.Equal(t, int64(0), PledgeOf(id, backerA)) uassert.Equal(t, b.backed, TotalBacked()) uassert.Equal(t, b.held, Held()) // Nothing left to unpledge. testing.SetRealm(testing.NewUserRealm(backerA)) uassert.AbortsWithMessage(t, cur, "no pledge to return", func() { Unpledge(cross(cur), id) }) testing.SetRealm(testing.NewUserRealm(creator1)) CreatorCancel(cross(cur), id) } // --- claim --- func TestClaimPaysCreatorMinusFee(cur realm, t *testing.T) { b := begin() id := launchAs(cur, creator1, "funded project") pledgeAs(cur, backerA, id, 900_000) pledgeAs(cur, backerB, id, 350_000) // raised 1_250_000 >= goal 1_000_000 testing.SkipHeights(MinDurationBlocks) uassert.Equal(t, "succeeded (awaiting creator claim)", Status(id)) uassert.False(t, IsRefundable(id)) // the pot is the creator's to collect // A backer cannot refund out of a succeeded campaign in-window. testing.SetRealm(testing.NewUserRealm(backerA)) uassert.AbortsWithMessage(t, cur, "campaign is not refundable", func() { Refund(cross(cur), id) }) // Only the creator may claim. testing.SetRealm(testing.NewUserRealm(mallory)) uassert.AbortsWithMessage(t, cur, "creator only", func() { CreatorClaim(cross(cur), id) }) walletBefore := balanceOf(creator1) openBefore := OpenCampaigns() testing.SetRealm(testing.NewUserRealm(creator1)) CreatorClaim(cross(cur), id) // fee = floor(1_250_000 * 100 / 10_000) = 12_500; credited the rest. uassert.Equal(t, walletBefore+1_237_500, balanceOf(creator1)) uassert.Equal(t, b.fees+12_500, FeesAccrued()) uassert.Equal(t, b.backed, TotalBacked()) uassert.Equal(t, statePaid, Status(id)) uassert.Equal(t, openBefore-1, OpenCampaigns()) uassert.Equal(t, int64(0), OpenOf(creator1)) // The pledge table is dropped; the final backer count is frozen. uassert.Equal(t, int64(0), PledgeOf(id, backerA)) _, _, raised, _, _, _, _, backers, state := CampaignInfo(id) uassert.Equal(t, int64(0), raised) uassert.Equal(t, 2, backers) uassert.Equal(t, statePaid, state) // Settlement is exactly-once: the deferral is consumed. testing.SetRealm(testing.NewUserRealm(creator1)) uassert.AbortsWithMessage(t, cur, "campaign is settled", func() { CreatorClaim(cross(cur), id) }) // Conservation: H gained the fee, B is flat, the payout left. uassert.Equal(t, b.held+12_500, Held()) uassert.True(t, Held() >= Liabilities()) } func TestClaimFeeRoundingFavorsCreator(cur realm, t *testing.T) { b := begin() testing.SetRealm(testing.NewUserRealm(creator1)) id := Launch(cross(cur), "rounding case", "", MinGoal, MinDurationBlocks) // 1_000_050 at 100 bps: exact fee 10_000.5 -> floor 10_000. pledgeAs(cur, backerA, id, 1_000_050) testing.SkipHeights(MinDurationBlocks) walletBefore := balanceOf(creator1) testing.SetRealm(testing.NewUserRealm(creator1)) CreatorClaim(cross(cur), id) uassert.Equal(t, walletBefore+990_050, balanceOf(creator1)) uassert.Equal(t, b.fees+10_000, FeesAccrued()) } func TestClaimGoalNotReached(cur realm, t *testing.T) { begin() id := launchAs(cur, creator1, "underfunded") pledgeAs(cur, backerA, id, MinGoal-1) testing.SkipHeights(MinDurationBlocks) testing.SetRealm(testing.NewUserRealm(creator1)) uassert.AbortsWithMessage(t, cur, "goal not reached", func() { CreatorClaim(cross(cur), id) }) // The failed pot is refundable immediately — no settlement needed. uassert.True(t, IsRefundable(id)) testing.SetRealm(testing.NewUserRealm(backerA)) Refund(cross(cur), id) testing.SetRealm(testing.NewUserRealm(mallory)) SettleFailed(cross(cur), id) // anyone closes it out; slot freed } func TestClaimTimingIsTheDuebooksVerdict(cur realm, t *testing.T) { begin() id := launchAs(cur, creator1, "timing case") pledgeAs(cur, backerA, id, MinGoal) // Before the deadline: the duebook refuses, and on-chain the whole // transaction would revert. In the test VM the abort does not roll // back realm globals, but MustClaim aborts BEFORE this realm writes // any state, so the campaign is observably untouched either way — // asserted below. testing.SetRealm(testing.NewUserRealm(creator1)) uassert.AbortsWithMessage(t, cur, "duebook: not due yet", func() { CreatorClaim(cross(cur), id) }) uassert.Equal(t, stateFunding, Status(id)) // still funding, untouched uassert.Equal(t, MinGoal, PledgeOf(id, backerA)) // Past the claim window: expired, the creator's authorization died. testing.SkipHeights(MinDurationBlocks + ClaimWindowBlocks) testing.SetRealm(testing.NewUserRealm(creator1)) uassert.AbortsWithMessage(t, cur, "duebook: deferral has expired", func() { CreatorClaim(cross(cur), id) }) // Which is exactly when Lapse starts working — by anyone. testing.SetRealm(testing.NewUserRealm(mallory)) Lapse(cross(cur), id) uassert.Equal(t, stateLapsed, Status(id)) uassert.True(t, IsRefundable(id)) testing.SetRealm(testing.NewUserRealm(backerA)) Refund(cross(cur), id) } // --- settle-failed (audit Y1 remediation) --- func TestSettleFailedFreesTheSlotAtTheDeadline(cur realm, t *testing.T) { begin() id := launchAs(cur, creator1, "doomed project") pledgeAs(cur, backerA, id, MinGoal-10_000) // Not before the deadline — the timing verdict is the duebook's. testing.SetRealm(testing.NewUserRealm(mallory)) uassert.AbortsWithMessage(t, cur, "duebook: not due yet", func() { SettleFailed(cross(cur), id) }) testing.SkipHeights(MinDurationBlocks) openBefore := OpenCampaigns() uassert.Equal(t, int64(1), OpenOf(creator1)) // Anyone may settle a failed campaign the moment it is due. testing.SetRealm(testing.NewUserRealm(mallory)) SettleFailed(cross(cur), id) uassert.Equal(t, stateFailed, Status(id)) uassert.Equal(t, openBefore-1, OpenCampaigns()) // duebook slot freed uassert.Equal(t, int64(0), OpenOf(creator1)) // creator slot freed uassert.True(t, IsRefundable(id)) // Settlement is exactly-once here too. testing.SetRealm(testing.NewUserRealm(mallory)) uassert.AbortsWithMessage(t, cur, "campaign is settled", func() { SettleFailed(cross(cur), id) }) // And the creator cannot claim a settled campaign. testing.SetRealm(testing.NewUserRealm(creator1)) uassert.AbortsWithMessage(t, cur, "campaign is settled", func() { CreatorClaim(cross(cur), id) }) // Refunds work exactly as on the un-settled failed path, and the // drained record is prunable. walletBefore := balanceOf(backerA) testing.SetRealm(testing.NewUserRealm(backerA)) Refund(cross(cur), id) uassert.Equal(t, walletBefore+MinGoal-10_000, balanceOf(backerA)) testing.SetRealm(testing.NewUserRealm(mallory)) Prune(cross(cur), id) } func TestSettleFailedExpiresIntoLapse(cur realm, t *testing.T) { begin() id := launchAs(cur, creator1, "never settled") pledgeAs(cur, backerA, id, MinPledge) testing.SkipHeights(MinDurationBlocks + ClaimWindowBlocks) // Past the claim window the deferral is expired: SettleFailed's // path is gone (the primitive's verdict) and Lapse is the valve — // which is exactly what the campaign page now advises (audit G1). page := Render(itoa(id)) uassert.True(t, strings.Contains(page, "Lapse("+itoa(id)+")")) uassert.False(t, strings.Contains(page, "SettleFailed(")) testing.SetRealm(testing.NewUserRealm(mallory)) uassert.AbortsWithMessage(t, cur, "duebook: deferral has expired", func() { SettleFailed(cross(cur), id) }) testing.SetRealm(testing.NewUserRealm(mallory)) Lapse(cross(cur), id) uassert.Equal(t, stateLapsed, Status(id)) testing.SetRealm(testing.NewUserRealm(backerA)) Refund(cross(cur), id) } func TestSettleFailedRefusesAMetGoal(cur realm, t *testing.T) { begin() id := launchAs(cur, creator1, "funded, not failed") pledgeAs(cur, backerA, id, MinGoal) testing.SkipHeights(MinDurationBlocks) testing.SetRealm(testing.NewUserRealm(mallory)) uassert.AbortsWithMessage(t, cur, "goal reached; the claim window is the creator's", func() { SettleFailed(cross(cur), id) }) testing.SetRealm(testing.NewUserRealm(creator1)) CreatorClaim(cross(cur), id) } // --- cancel --- func TestCancelOpensRefunds(cur realm, t *testing.T) { b := begin() id := launchAs(cur, creator1, "cancelled project") pledgeAs(cur, backerA, id, 600_000) // Mallory cannot cancel someone else's campaign. testing.SetRealm(testing.NewUserRealm(mallory)) uassert.AbortsWithMessage(t, cur, "creator only", func() { CreatorCancel(cross(cur), id) }) testing.SetRealm(testing.NewUserRealm(creator1)) CreatorCancel(cross(cur), id) uassert.Equal(t, stateCancelled, Status(id)) uassert.True(t, IsRefundable(id)) // Cancelling twice: the deferral is gone, the state says so first. testing.SetRealm(testing.NewUserRealm(creator1)) uassert.AbortsWithMessage(t, cur, "campaign is settled", func() { CreatorCancel(cross(cur), id) }) walletBefore := balanceOf(backerA) testing.SetRealm(testing.NewUserRealm(backerA)) Refund(cross(cur), id) uassert.Equal(t, walletBefore+600_000, balanceOf(backerA)) uassert.Equal(t, b.backed, TotalBacked()) uassert.Equal(t, b.fees, FeesAccrued()) // refunds are fee-free } func TestCancelAfterSuccessRenouncesThePot(cur realm, t *testing.T) { begin() id := launchAs(cur, creator1, "renounced project") pledgeAs(cur, backerA, id, MinGoal) testing.SkipHeights(MinDurationBlocks) uassert.Equal(t, "succeeded (awaiting creator claim)", Status(id)) testing.SetRealm(testing.NewUserRealm(creator1)) CreatorCancel(cross(cur), id) uassert.True(t, IsRefundable(id)) testing.SetRealm(testing.NewUserRealm(backerA)) Refund(cross(cur), id) uassert.Equal(t, int64(0), PledgeOf(id, backerA)) } // --- lapse --- func TestLapseRefusesWhileClaimWindowIsOpen(cur realm, t *testing.T) { begin() id := launchAs(cur, creator1, "lapse timing") pledgeAs(cur, backerA, id, MinGoal) testing.SkipHeights(MinDurationBlocks) // due, but window open testing.SetRealm(testing.NewUserRealm(mallory)) uassert.AbortsWithMessage(t, cur, "duebook: deferral has not expired", func() { Lapse(cross(cur), id) }) testing.SkipHeights(ClaimWindowBlocks) testing.SetRealm(testing.NewUserRealm(mallory)) Lapse(cross(cur), id) uassert.Equal(t, stateLapsed, Status(id)) testing.SetRealm(testing.NewUserRealm(mallory)) uassert.AbortsWithMessage(t, cur, "campaign is settled", func() { Lapse(cross(cur), id) }) testing.SetRealm(testing.NewUserRealm(backerA)) Refund(cross(cur), id) } // --- refund guards --- func TestRefundGuards(cur realm, t *testing.T) { begin() id := launchAs(cur, creator1, "refund guards") pledgeAs(cur, backerA, id, 100_000) // Not refundable while funding is open. testing.SetRealm(testing.NewUserRealm(backerA)) uassert.AbortsWithMessage(t, cur, "campaign is not refundable", func() { Refund(cross(cur), id) }) testing.SetRealm(testing.NewUserRealm(creator1)) CreatorCancel(cross(cur), id) // A stranger with no pledge gets nothing. testing.SetRealm(testing.NewUserRealm(mallory)) uassert.AbortsWithMessage(t, cur, "no pledge to return", func() { Refund(cross(cur), id) }) // The backer refunds once, then has nothing left. testing.SetRealm(testing.NewUserRealm(backerA)) Refund(cross(cur), id) testing.SetRealm(testing.NewUserRealm(backerA)) uassert.AbortsWithMessage(t, cur, "no pledge to return", func() { Refund(cross(cur), id) }) } // --- prune --- func TestPrune(cur realm, t *testing.T) { begin() id := launchAs(cur, creator1, "prunable") pledgeAs(cur, backerA, id, 50_000) // Not while unsettled. testing.SetRealm(testing.NewUserRealm(mallory)) uassert.AbortsWithMessage(t, cur, "campaign is not settled", func() { Prune(cross(cur), id) }) testing.SetRealm(testing.NewUserRealm(creator1)) CreatorCancel(cross(cur), id) // Not while backer funds remain. testing.SetRealm(testing.NewUserRealm(mallory)) uassert.AbortsWithMessage(t, cur, "campaign still holds backer funds", func() { Prune(cross(cur), id) }) testing.SetRealm(testing.NewUserRealm(backerA)) Refund(cross(cur), id) nBefore := NumCampaigns() testing.SetRealm(testing.NewUserRealm(mallory)) Prune(cross(cur), id) uassert.Equal(t, nBefore, NumCampaigns()) // ids are never reused testing.SetRealm(testing.NewUserRealm(mallory)) uassert.AbortsWithMessage(t, cur, "unknown campaign id", func() { Prune(cross(cur), id) }) uassert.PanicsWithMessage(t, cur, "unknown campaign id", func() { CampaignInfo(id) }) } // --- fees and surplus --- func TestWithdrawFees(cur realm, t *testing.T) { b := begin() if b.fees == 0 { // Accrue a fee: fund and claim a campaign. id := launchAs(cur, creator1, "fee source") pledgeAs(cur, backerA, id, MinGoal) testing.SkipHeights(MinDurationBlocks) testing.SetRealm(testing.NewUserRealm(creator1)) CreatorClaim(cross(cur), id) } fees := FeesAccrued() uassert.True(t, fees > 0) collectorBefore := balanceOf(FeeCollector) testing.SetRealm(testing.NewUserRealm(mallory)) // anyone may trigger got := WithdrawFees(cross(cur)) uassert.Equal(t, fees, got) uassert.Equal(t, collectorBefore+fees, balanceOf(FeeCollector)) uassert.Equal(t, int64(0), FeesAccrued()) testing.SetRealm(testing.NewUserRealm(mallory)) uassert.AbortsWithMessage(t, cur, "no fees accrued", func() { WithdrawFees(cross(cur)) }) } func TestSweepSurplus(cur realm, t *testing.T) { begin() // No surplus: coinio refuses. testing.SetRealm(testing.NewUserRealm(mallory)) uassert.AbortsWithMessage(t, cur, "coinio: no surplus to sweep for ugnot", func() { SweepSurplus(cross(cur), Denom) }) // Force-send 33_000 ugnot out-of-band, then sweep it. testing.IssueCoins(Address(), ugnot(33_000)) liabBefore := Liabilities() collectorBefore := balanceOf(FeeCollector) testing.SetRealm(testing.NewUserRealm(mallory)) swept := SweepSurplus(cross(cur), Denom) uassert.Equal(t, int64(33_000), swept) uassert.Equal(t, collectorBefore+33_000, balanceOf(FeeCollector)) uassert.Equal(t, liabBefore, Liabilities()) // tracked money untouched // A foreign denomination sweeps in full. testing.IssueCoins(Address(), chain.NewCoins(chain.NewCoin("foocoin", 4_200))) testing.SetRealm(testing.NewUserRealm(mallory)) uassert.Equal(t, int64(4_200), SweepSurplus(cross(cur), "foocoin")) } // --- stray sends --- func TestNonPayableEntrypointsRejectCoins(cur realm, t *testing.T) { begin() id := launchAs(cur, creator1, "stray send case") cases := []func(){ func() { Launch(cross(cur), "t", "", MinGoal, MinDurationBlocks) }, func() { Unpledge(cross(cur), id) }, func() { CreatorClaim(cross(cur), id) }, func() { CreatorCancel(cross(cur), id) }, func() { SettleFailed(cross(cur), id) }, func() { Lapse(cross(cur), id) }, func() { Refund(cross(cur), id) }, func() { Prune(cross(cur), id) }, func() { WithdrawFees(cross(cur)) }, func() { SweepSurplus(cross(cur), Denom) }, } for _, call := range cases { testing.SetRealm(testing.NewUserRealm(mallory)) testing.SetOriginSend(ugnot(1)) uassert.AbortsWithMessage(t, cur, "this entrypoint does not accept coins", call) } testing.SetOriginSend(nil) testing.SetRealm(testing.NewUserRealm(creator1)) CreatorCancel(cross(cur), id) } // --- render --- func TestRenderIndexAndCampaign(cur realm, t *testing.T) { begin() // A hostile title must come out of the sanitizer defanged. hostile := "[evil](https://x) " testing.SetRealm(testing.NewUserRealm(creator1)) id := Launch(cross(cur), hostile, "memo with [link](x) inside", MinGoal, MinDurationBlocks) index := Render("") uassert.True(t, strings.Contains(index, "# Crowdfund")) uassert.True(t, strings.Contains(index, "conservation: OK")) uassert.False(t, strings.Contains(index, "