1package crowdfund23import (4 "strings"5 "testing"67 "chain"89 "gno.land/p/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/coinio"10 "gno.land/p/nt/testutils/v0"11 "gno.land/p/nt/uassert/v0"12)1314var (15 creator1 = testutils.TestAddress("cfcreator1")16 creator2 = testutils.TestAddress("cfcreator2")17 backerA = testutils.TestAddress("cfbackera")18 backerB = testutils.TestAddress("cfbackerb")19 mallory = testutils.TestAddress("cfmallory")20)2122// HARNESS NOTES, carried from the treasury_board, vesting and grants23// suites (measured there, relied on here):24//25// - testing.SetRealm records the override per FRAME INDEX: stage the26// caller INLINE in the test function's frame before a uassert call,27// or inside a helper that stages and calls in one frame of its own.28// Never inside a uassert closure — that staging does not take.29//30// - testing.SkipHeights REPLACES the whole context (OriginCaller,31// CurrentRealm, OriginSend included). Re-stage after every skip.32//33// - Realm globals PERSIST across tests while banker state resets per34// test. begin() disarms any leftover envelope and re-seeds the35// realm's bank to its carried liabilities, so conservation is36// asserted in DELTA form.37//38// - A panic caught by uassert does NOT roll back realm globals here,39// whereas on-chain the whole transaction reverts. Every abort40// asserted below happens strictly BEFORE this realm writes state,41// with one measured exception documented inline42// (TestClaimTimingIsTheDuebooksVerdict).43//44// - The SEND envelope is process-global: a staged envelope must be45// cleared (SetOriginSend(nil)) before the next non-payable call, or46// rejectStraySend fires and the test asserts the harness leak, not47// the guard.48func init() {49 self = chain.PackageAddress("gno.land/r/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/crowdfund")50}5152type baseline struct {53 held int6454 backed int6455 fees int6456}5758func begin() baseline {59 testing.SetOriginSend(nil)60 if l := Liabilities(); l > 0 {61 testing.IssueCoins(Address(), ugnot(l))62 }63 return baseline{held: Held(), backed: TotalBacked(), fees: FeesAccrued()}64}6566func ugnot(n int64) chain.Coins {67 return chain.NewCoins(chain.NewCoin(Denom, n))68}6970// launchAs stages `from` and launches a MinGoal x MinDuration campaign.71func launchAs(cur realm, from address, title string) int64 {72 testing.SetRealm(testing.NewUserRealm(from))73 return Launch(cross(cur), title, "", MinGoal, MinDurationBlocks)74}7576// pledgeAs stages a direct EOA call with -send and mirrors the77// envelope into the realm's bank, as the chain would.78func pledgeAs(cur realm, from address, id, amount int64) {79 testing.SetRealm(testing.NewUserRealm(from))80 testing.SetOriginSend(ugnot(amount))81 testing.IssueCoins(Address(), ugnot(amount))82 Pledge(cross(cur), id)83 testing.SetOriginSend(nil)84}8586func balanceOf(addr address) int64 {87 return coinio.HeldAt(addr, Denom)88}8990// --- launch ---9192func TestLaunchBasics(cur realm, t *testing.T) {93 begin()94 before := NumCampaigns()95 openBefore := OpenCampaigns()9697 id := launchAs(cur, creator1, "Solar panels for the hackerspace")98 uassert.Equal(t, before+1, id)99 uassert.Equal(t, before+1, NumCampaigns())100 uassert.Equal(t, openBefore+1, OpenCampaigns())101 uassert.Equal(t, int64(1), OpenOf(creator1))102103 cr, goal, raised, feeBps, createdAt, deadline, claimEnd, backers, state := CampaignInfo(id)104 uassert.Equal(t, creator1, cr)105 uassert.Equal(t, MinGoal, goal)106 uassert.Equal(t, int64(0), raised)107 uassert.Equal(t, SuccessFeeBps, feeBps)108 uassert.Equal(t, createdAt+MinDurationBlocks, deadline)109 uassert.Equal(t, deadline+ClaimWindowBlocks, claimEnd)110 uassert.Equal(t, 0, backers)111 uassert.Equal(t, stateFunding, state)112 uassert.Equal(t, stateFunding, Status(id))113114 // Clean up the open slot so later per-creator-cap tests see a known115 // count.116 testing.SetRealm(testing.NewUserRealm(creator1))117 CreatorCancel(cross(cur), id)118 uassert.Equal(t, int64(0), OpenOf(creator1))119}120121func TestLaunchValidation(cur realm, t *testing.T) {122 begin()123 testing.SetRealm(testing.NewUserRealm(creator1))124 uassert.AbortsWithMessage(t, cur, "title must not be empty", func() {125 Launch(cross(cur), "", "", MinGoal, MinDurationBlocks)126 })127 testing.SetRealm(testing.NewUserRealm(creator1))128 uassert.AbortsWithMessage(t, cur, "title exceeds 100 bytes", func() {129 Launch(cross(cur), strings.Repeat("x", MaxTitleLen+1), "", MinGoal, MinDurationBlocks)130 })131 testing.SetRealm(testing.NewUserRealm(creator1))132 uassert.AbortsWithMessage(t, cur, "memo exceeds 256 bytes", func() {133 Launch(cross(cur), "t", strings.Repeat("x", MaxMemoLen+1), MinGoal, MinDurationBlocks)134 })135 testing.SetRealm(testing.NewUserRealm(creator1))136 uassert.AbortsWithMessage(t, cur, "goal must be at least 1000000ugnot", func() {137 Launch(cross(cur), "t", "", MinGoal-1, MinDurationBlocks)138 })139 // Duration bounds are the duebook's own validation, not a check in140 // this realm — asserted against the primitive's error text.141 testing.SetRealm(testing.NewUserRealm(creator1))142 uassert.AbortsWithMessage(t, cur, "duebook: delay outside [minDelay, maxDelay]", func() {143 Launch(cross(cur), "t", "", MinGoal, MinDurationBlocks-1)144 })145 testing.SetRealm(testing.NewUserRealm(creator1))146 uassert.AbortsWithMessage(t, cur, "duebook: delay outside [minDelay, maxDelay]", func() {147 Launch(cross(cur), "t", "", MinGoal, MaxDurationBlocks+1)148 })149 uassert.Equal(t, int64(0), OpenOf(creator1))150}151152func TestLaunchPerCreatorCap(cur realm, t *testing.T) {153 begin()154 uassert.Equal(t, int64(0), OpenOf(creator2))155 ids := []int64{}156 for i := int64(0); i < MaxOpenPerCreator; i++ {157 ids = append(ids, launchAs(cur, creator2, "cap filler"))158 }159 uassert.Equal(t, MaxOpenPerCreator, OpenOf(creator2))160161 testing.SetRealm(testing.NewUserRealm(creator2))162 uassert.AbortsWithMessage(t, cur, "creator already has 8 unsettled campaigns", func() {163 Launch(cross(cur), "one too many", "", MinGoal, MinDurationBlocks)164 })165166 // A settled campaign releases its slot.167 testing.SetRealm(testing.NewUserRealm(creator2))168 CreatorCancel(cross(cur), ids[0])169 uassert.Equal(t, MaxOpenPerCreator-1, OpenOf(creator2))170 idNew := launchAs(cur, creator2, "slot reopened")171 uassert.Equal(t, MaxOpenPerCreator, OpenOf(creator2))172173 // Drain the slots so later tests start from a clean count.174 for _, id := range append(ids[1:], idNew) {175 testing.SetRealm(testing.NewUserRealm(creator2))176 CreatorCancel(cross(cur), id)177 }178 uassert.Equal(t, int64(0), OpenOf(creator2))179}180181// --- pledging ---182183func TestPledgeAndConservation(cur realm, t *testing.T) {184 b := begin()185 id := launchAs(cur, creator1, "conservation case")186187 pledgeAs(cur, backerA, id, 400_000)188 pledgeAs(cur, backerB, id, 250_000)189 pledgeAs(cur, backerA, id, 100_000) // same backer accumulates190191 uassert.Equal(t, int64(500_000), PledgeOf(id, backerA))192 uassert.Equal(t, int64(250_000), PledgeOf(id, backerB))193 _, _, raised, _, _, _, _, backers, _ := CampaignInfo(id)194 uassert.Equal(t, int64(750_000), raised)195 uassert.Equal(t, 2, backers)196197 // Conservation, delta form: every pledged coin is in B and in H.198 uassert.Equal(t, b.backed+750_000, TotalBacked())199 uassert.Equal(t, b.held+750_000, Held())200 uassert.Equal(t, b.fees, FeesAccrued())201 uassert.True(t, Held() >= Liabilities())202203 // Clean up: back out both pledges, cancel.204 testing.SetRealm(testing.NewUserRealm(backerA))205 Unpledge(cross(cur), id)206 testing.SetRealm(testing.NewUserRealm(backerB))207 Unpledge(cross(cur), id)208 uassert.Equal(t, b.backed, TotalBacked())209 testing.SetRealm(testing.NewUserRealm(creator1))210 CreatorCancel(cross(cur), id)211}212213func TestPledgeGuards(cur realm, t *testing.T) {214 begin()215 id := launchAs(cur, creator1, "guard case")216217 // The payment-guard regression: a realm-routed pledge is refused by218 // coinio's receipt shape, not by anything this file could forget.219 testing.SetRealm(testing.NewCodeRealm("gno.land/r/demo/attacker"))220 testing.SetOriginSend(ugnot(1000))221 uassert.AbortsWithMessage(t, cur,222 "coinio: payment must be a direct EOA call with -send (realms and maketx-run are rejected)",223 func() { Pledge(cross(cur), id) })224 testing.SetOriginSend(nil)225226 // Wrong denomination.227 testing.SetRealm(testing.NewUserRealm(backerA))228 testing.SetOriginSend(chain.NewCoins(chain.NewCoin("foocoin", 1000)))229 uassert.AbortsWithMessage(t, cur, "coinio: send exactly one coin type: ugnot", func() {230 Pledge(cross(cur), id)231 })232 testing.SetOriginSend(nil)233234 // No envelope at all.235 testing.SetRealm(testing.NewUserRealm(backerA))236 uassert.AbortsWithMessage(t, cur, "coinio: send exactly one coin type: ugnot", func() {237 Pledge(cross(cur), id)238 })239240 // Unknown campaign.241 testing.SetRealm(testing.NewUserRealm(backerA))242 testing.SetOriginSend(ugnot(1000))243 uassert.AbortsWithMessage(t, cur, "unknown campaign id", func() {244 Pledge(cross(cur), 999_999)245 })246 testing.SetOriginSend(nil)247248 // Below the pledge minimum (audit open question 1: dust-entry249 // bloat); exactly the minimum is accepted.250 testing.SetRealm(testing.NewUserRealm(backerA))251 testing.SetOriginSend(ugnot(MinPledge - 1))252 testing.IssueCoins(Address(), ugnot(MinPledge-1))253 uassert.AbortsWithMessage(t, cur, "pledge at least 10000ugnot", func() {254 Pledge(cross(cur), id)255 })256 testing.SetOriginSend(nil)257 pledgeAs(cur, backerA, id, MinPledge)258 uassert.Equal(t, MinPledge, PledgeOf(id, backerA))259 testing.SetRealm(testing.NewUserRealm(backerA))260 Unpledge(cross(cur), id)261262 // Settled campaign.263 testing.SetRealm(testing.NewUserRealm(creator1))264 CreatorCancel(cross(cur), id)265 testing.SetRealm(testing.NewUserRealm(backerA))266 testing.SetOriginSend(ugnot(1000))267 uassert.AbortsWithMessage(t, cur, "campaign is settled", func() {268 Pledge(cross(cur), id)269 })270 testing.SetOriginSend(nil)271}272273func TestPledgeClosesAtDeadline(cur realm, t *testing.T) {274 begin()275 id := launchAs(cur, creator1, "deadline case")276 testing.SkipHeights(MinDurationBlocks) // height == deadline exactly277278 testing.SetRealm(testing.NewUserRealm(backerA))279 testing.SetOriginSend(ugnot(1000))280 testing.IssueCoins(Address(), ugnot(1000))281 uassert.AbortsWithMessage(t, cur, "funding is closed", func() {282 Pledge(cross(cur), id)283 })284 testing.SetOriginSend(nil)285286 // Unpledge is likewise closed at the deadline (the failed path is287 // Refund's).288 testing.SetRealm(testing.NewUserRealm(backerA))289 uassert.AbortsWithMessage(t, cur,290 "funding is closed; use Refund if the campaign failed",291 func() { Unpledge(cross(cur), id) })292293 // Failed (0 < goal) and past deadline: refunds open, nothing to pay.294 uassert.True(t, IsRefundable(id))295 uassert.Equal(t, "failed (refunds open)", Status(id))296297 testing.SetRealm(testing.NewUserRealm(creator1))298 CreatorCancel(cross(cur), id)299}300301// --- unpledge ---302303func TestUnpledgeReturnsWholePledge(cur realm, t *testing.T) {304 b := begin()305 id := launchAs(cur, creator1, "unpledge case")306 pledgeAs(cur, backerA, id, 300_000)307308 walletBefore := balanceOf(backerA)309 testing.SetRealm(testing.NewUserRealm(backerA))310 Unpledge(cross(cur), id)311312 uassert.Equal(t, walletBefore+300_000, balanceOf(backerA))313 uassert.Equal(t, int64(0), PledgeOf(id, backerA))314 uassert.Equal(t, b.backed, TotalBacked())315 uassert.Equal(t, b.held, Held())316317 // Nothing left to unpledge.318 testing.SetRealm(testing.NewUserRealm(backerA))319 uassert.AbortsWithMessage(t, cur, "no pledge to return", func() {320 Unpledge(cross(cur), id)321 })322 testing.SetRealm(testing.NewUserRealm(creator1))323 CreatorCancel(cross(cur), id)324}325326// --- claim ---327328func TestClaimPaysCreatorMinusFee(cur realm, t *testing.T) {329 b := begin()330 id := launchAs(cur, creator1, "funded project")331 pledgeAs(cur, backerA, id, 900_000)332 pledgeAs(cur, backerB, id, 350_000) // raised 1_250_000 >= goal 1_000_000333334 testing.SkipHeights(MinDurationBlocks)335 uassert.Equal(t, "succeeded (awaiting creator claim)", Status(id))336 uassert.False(t, IsRefundable(id)) // the pot is the creator's to collect337338 // A backer cannot refund out of a succeeded campaign in-window.339 testing.SetRealm(testing.NewUserRealm(backerA))340 uassert.AbortsWithMessage(t, cur, "campaign is not refundable", func() {341 Refund(cross(cur), id)342 })343344 // Only the creator may claim.345 testing.SetRealm(testing.NewUserRealm(mallory))346 uassert.AbortsWithMessage(t, cur, "creator only", func() {347 CreatorClaim(cross(cur), id)348 })349350 walletBefore := balanceOf(creator1)351 openBefore := OpenCampaigns()352 testing.SetRealm(testing.NewUserRealm(creator1))353 CreatorClaim(cross(cur), id)354355 // fee = floor(1_250_000 * 100 / 10_000) = 12_500; credited the rest.356 uassert.Equal(t, walletBefore+1_237_500, balanceOf(creator1))357 uassert.Equal(t, b.fees+12_500, FeesAccrued())358 uassert.Equal(t, b.backed, TotalBacked())359 uassert.Equal(t, statePaid, Status(id))360 uassert.Equal(t, openBefore-1, OpenCampaigns())361 uassert.Equal(t, int64(0), OpenOf(creator1))362363 // The pledge table is dropped; the final backer count is frozen.364 uassert.Equal(t, int64(0), PledgeOf(id, backerA))365 _, _, raised, _, _, _, _, backers, state := CampaignInfo(id)366 uassert.Equal(t, int64(0), raised)367 uassert.Equal(t, 2, backers)368 uassert.Equal(t, statePaid, state)369370 // Settlement is exactly-once: the deferral is consumed.371 testing.SetRealm(testing.NewUserRealm(creator1))372 uassert.AbortsWithMessage(t, cur, "campaign is settled", func() {373 CreatorClaim(cross(cur), id)374 })375 // Conservation: H gained the fee, B is flat, the payout left.376 uassert.Equal(t, b.held+12_500, Held())377 uassert.True(t, Held() >= Liabilities())378}379380func TestClaimFeeRoundingFavorsCreator(cur realm, t *testing.T) {381 b := begin()382 testing.SetRealm(testing.NewUserRealm(creator1))383 id := Launch(cross(cur), "rounding case", "", MinGoal, MinDurationBlocks)384 // 1_000_050 at 100 bps: exact fee 10_000.5 -> floor 10_000.385 pledgeAs(cur, backerA, id, 1_000_050)386 testing.SkipHeights(MinDurationBlocks)387388 walletBefore := balanceOf(creator1)389 testing.SetRealm(testing.NewUserRealm(creator1))390 CreatorClaim(cross(cur), id)391 uassert.Equal(t, walletBefore+990_050, balanceOf(creator1))392 uassert.Equal(t, b.fees+10_000, FeesAccrued())393}394395func TestClaimGoalNotReached(cur realm, t *testing.T) {396 begin()397 id := launchAs(cur, creator1, "underfunded")398 pledgeAs(cur, backerA, id, MinGoal-1)399 testing.SkipHeights(MinDurationBlocks)400401 testing.SetRealm(testing.NewUserRealm(creator1))402 uassert.AbortsWithMessage(t, cur, "goal not reached", func() {403 CreatorClaim(cross(cur), id)404 })405406 // The failed pot is refundable immediately — no settlement needed.407 uassert.True(t, IsRefundable(id))408 testing.SetRealm(testing.NewUserRealm(backerA))409 Refund(cross(cur), id)410 testing.SetRealm(testing.NewUserRealm(mallory))411 SettleFailed(cross(cur), id) // anyone closes it out; slot freed412}413414func TestClaimTimingIsTheDuebooksVerdict(cur realm, t *testing.T) {415 begin()416 id := launchAs(cur, creator1, "timing case")417 pledgeAs(cur, backerA, id, MinGoal)418419 // Before the deadline: the duebook refuses, and on-chain the whole420 // transaction would revert. In the test VM the abort does not roll421 // back realm globals, but MustClaim aborts BEFORE this realm writes422 // any state, so the campaign is observably untouched either way —423 // asserted below.424 testing.SetRealm(testing.NewUserRealm(creator1))425 uassert.AbortsWithMessage(t, cur, "duebook: not due yet", func() {426 CreatorClaim(cross(cur), id)427 })428 uassert.Equal(t, stateFunding, Status(id)) // still funding, untouched429 uassert.Equal(t, MinGoal, PledgeOf(id, backerA))430431 // Past the claim window: expired, the creator's authorization died.432 testing.SkipHeights(MinDurationBlocks + ClaimWindowBlocks)433 testing.SetRealm(testing.NewUserRealm(creator1))434 uassert.AbortsWithMessage(t, cur, "duebook: deferral has expired", func() {435 CreatorClaim(cross(cur), id)436 })437438 // Which is exactly when Lapse starts working — by anyone.439 testing.SetRealm(testing.NewUserRealm(mallory))440 Lapse(cross(cur), id)441 uassert.Equal(t, stateLapsed, Status(id))442 uassert.True(t, IsRefundable(id))443444 testing.SetRealm(testing.NewUserRealm(backerA))445 Refund(cross(cur), id)446}447448// --- settle-failed (audit Y1 remediation) ---449450func TestSettleFailedFreesTheSlotAtTheDeadline(cur realm, t *testing.T) {451 begin()452 id := launchAs(cur, creator1, "doomed project")453 pledgeAs(cur, backerA, id, MinGoal-10_000)454455 // Not before the deadline — the timing verdict is the duebook's.456 testing.SetRealm(testing.NewUserRealm(mallory))457 uassert.AbortsWithMessage(t, cur, "duebook: not due yet", func() {458 SettleFailed(cross(cur), id)459 })460461 testing.SkipHeights(MinDurationBlocks)462 openBefore := OpenCampaigns()463 uassert.Equal(t, int64(1), OpenOf(creator1))464465 // Anyone may settle a failed campaign the moment it is due.466 testing.SetRealm(testing.NewUserRealm(mallory))467 SettleFailed(cross(cur), id)468 uassert.Equal(t, stateFailed, Status(id))469 uassert.Equal(t, openBefore-1, OpenCampaigns()) // duebook slot freed470 uassert.Equal(t, int64(0), OpenOf(creator1)) // creator slot freed471 uassert.True(t, IsRefundable(id))472473 // Settlement is exactly-once here too.474 testing.SetRealm(testing.NewUserRealm(mallory))475 uassert.AbortsWithMessage(t, cur, "campaign is settled", func() {476 SettleFailed(cross(cur), id)477 })478 // And the creator cannot claim a settled campaign.479 testing.SetRealm(testing.NewUserRealm(creator1))480 uassert.AbortsWithMessage(t, cur, "campaign is settled", func() {481 CreatorClaim(cross(cur), id)482 })483484 // Refunds work exactly as on the un-settled failed path, and the485 // drained record is prunable.486 walletBefore := balanceOf(backerA)487 testing.SetRealm(testing.NewUserRealm(backerA))488 Refund(cross(cur), id)489 uassert.Equal(t, walletBefore+MinGoal-10_000, balanceOf(backerA))490 testing.SetRealm(testing.NewUserRealm(mallory))491 Prune(cross(cur), id)492}493494func TestSettleFailedExpiresIntoLapse(cur realm, t *testing.T) {495 begin()496 id := launchAs(cur, creator1, "never settled")497 pledgeAs(cur, backerA, id, MinPledge)498 testing.SkipHeights(MinDurationBlocks + ClaimWindowBlocks)499500 // Past the claim window the deferral is expired: SettleFailed's501 // path is gone (the primitive's verdict) and Lapse is the valve —502 // which is exactly what the campaign page now advises (audit G1).503 page := Render(itoa(id))504 uassert.True(t, strings.Contains(page, "Lapse("+itoa(id)+")"))505 uassert.False(t, strings.Contains(page, "SettleFailed("))506507 testing.SetRealm(testing.NewUserRealm(mallory))508 uassert.AbortsWithMessage(t, cur, "duebook: deferral has expired", func() {509 SettleFailed(cross(cur), id)510 })511 testing.SetRealm(testing.NewUserRealm(mallory))512 Lapse(cross(cur), id)513 uassert.Equal(t, stateLapsed, Status(id))514 testing.SetRealm(testing.NewUserRealm(backerA))515 Refund(cross(cur), id)516}517518func TestSettleFailedRefusesAMetGoal(cur realm, t *testing.T) {519 begin()520 id := launchAs(cur, creator1, "funded, not failed")521 pledgeAs(cur, backerA, id, MinGoal)522 testing.SkipHeights(MinDurationBlocks)523524 testing.SetRealm(testing.NewUserRealm(mallory))525 uassert.AbortsWithMessage(t, cur,526 "goal reached; the claim window is the creator's",527 func() { SettleFailed(cross(cur), id) })528529 testing.SetRealm(testing.NewUserRealm(creator1))530 CreatorClaim(cross(cur), id)531}532533// --- cancel ---534535func TestCancelOpensRefunds(cur realm, t *testing.T) {536 b := begin()537 id := launchAs(cur, creator1, "cancelled project")538 pledgeAs(cur, backerA, id, 600_000)539540 // Mallory cannot cancel someone else's campaign.541 testing.SetRealm(testing.NewUserRealm(mallory))542 uassert.AbortsWithMessage(t, cur, "creator only", func() {543 CreatorCancel(cross(cur), id)544 })545546 testing.SetRealm(testing.NewUserRealm(creator1))547 CreatorCancel(cross(cur), id)548 uassert.Equal(t, stateCancelled, Status(id))549 uassert.True(t, IsRefundable(id))550551 // Cancelling twice: the deferral is gone, the state says so first.552 testing.SetRealm(testing.NewUserRealm(creator1))553 uassert.AbortsWithMessage(t, cur, "campaign is settled", func() {554 CreatorCancel(cross(cur), id)555 })556557 walletBefore := balanceOf(backerA)558 testing.SetRealm(testing.NewUserRealm(backerA))559 Refund(cross(cur), id)560 uassert.Equal(t, walletBefore+600_000, balanceOf(backerA))561 uassert.Equal(t, b.backed, TotalBacked())562 uassert.Equal(t, b.fees, FeesAccrued()) // refunds are fee-free563}564565func TestCancelAfterSuccessRenouncesThePot(cur realm, t *testing.T) {566 begin()567 id := launchAs(cur, creator1, "renounced project")568 pledgeAs(cur, backerA, id, MinGoal)569 testing.SkipHeights(MinDurationBlocks)570 uassert.Equal(t, "succeeded (awaiting creator claim)", Status(id))571572 testing.SetRealm(testing.NewUserRealm(creator1))573 CreatorCancel(cross(cur), id)574 uassert.True(t, IsRefundable(id))575 testing.SetRealm(testing.NewUserRealm(backerA))576 Refund(cross(cur), id)577 uassert.Equal(t, int64(0), PledgeOf(id, backerA))578}579580// --- lapse ---581582func TestLapseRefusesWhileClaimWindowIsOpen(cur realm, t *testing.T) {583 begin()584 id := launchAs(cur, creator1, "lapse timing")585 pledgeAs(cur, backerA, id, MinGoal)586 testing.SkipHeights(MinDurationBlocks) // due, but window open587588 testing.SetRealm(testing.NewUserRealm(mallory))589 uassert.AbortsWithMessage(t, cur, "duebook: deferral has not expired", func() {590 Lapse(cross(cur), id)591 })592593 testing.SkipHeights(ClaimWindowBlocks)594 testing.SetRealm(testing.NewUserRealm(mallory))595 Lapse(cross(cur), id)596 uassert.Equal(t, stateLapsed, Status(id))597598 testing.SetRealm(testing.NewUserRealm(mallory))599 uassert.AbortsWithMessage(t, cur, "campaign is settled", func() {600 Lapse(cross(cur), id)601 })602 testing.SetRealm(testing.NewUserRealm(backerA))603 Refund(cross(cur), id)604}605606// --- refund guards ---607608func TestRefundGuards(cur realm, t *testing.T) {609 begin()610 id := launchAs(cur, creator1, "refund guards")611 pledgeAs(cur, backerA, id, 100_000)612613 // Not refundable while funding is open.614 testing.SetRealm(testing.NewUserRealm(backerA))615 uassert.AbortsWithMessage(t, cur, "campaign is not refundable", func() {616 Refund(cross(cur), id)617 })618619 testing.SetRealm(testing.NewUserRealm(creator1))620 CreatorCancel(cross(cur), id)621622 // A stranger with no pledge gets nothing.623 testing.SetRealm(testing.NewUserRealm(mallory))624 uassert.AbortsWithMessage(t, cur, "no pledge to return", func() {625 Refund(cross(cur), id)626 })627628 // The backer refunds once, then has nothing left.629 testing.SetRealm(testing.NewUserRealm(backerA))630 Refund(cross(cur), id)631 testing.SetRealm(testing.NewUserRealm(backerA))632 uassert.AbortsWithMessage(t, cur, "no pledge to return", func() {633 Refund(cross(cur), id)634 })635}636637// --- prune ---638639func TestPrune(cur realm, t *testing.T) {640 begin()641 id := launchAs(cur, creator1, "prunable")642 pledgeAs(cur, backerA, id, 50_000)643644 // Not while unsettled.645 testing.SetRealm(testing.NewUserRealm(mallory))646 uassert.AbortsWithMessage(t, cur, "campaign is not settled", func() {647 Prune(cross(cur), id)648 })649650 testing.SetRealm(testing.NewUserRealm(creator1))651 CreatorCancel(cross(cur), id)652653 // Not while backer funds remain.654 testing.SetRealm(testing.NewUserRealm(mallory))655 uassert.AbortsWithMessage(t, cur, "campaign still holds backer funds", func() {656 Prune(cross(cur), id)657 })658659 testing.SetRealm(testing.NewUserRealm(backerA))660 Refund(cross(cur), id)661662 nBefore := NumCampaigns()663 testing.SetRealm(testing.NewUserRealm(mallory))664 Prune(cross(cur), id)665 uassert.Equal(t, nBefore, NumCampaigns()) // ids are never reused666 testing.SetRealm(testing.NewUserRealm(mallory))667 uassert.AbortsWithMessage(t, cur, "unknown campaign id", func() {668 Prune(cross(cur), id)669 })670 uassert.PanicsWithMessage(t, cur, "unknown campaign id", func() {671 CampaignInfo(id)672 })673}674675// --- fees and surplus ---676677func TestWithdrawFees(cur realm, t *testing.T) {678 b := begin()679 if b.fees == 0 {680 // Accrue a fee: fund and claim a campaign.681 id := launchAs(cur, creator1, "fee source")682 pledgeAs(cur, backerA, id, MinGoal)683 testing.SkipHeights(MinDurationBlocks)684 testing.SetRealm(testing.NewUserRealm(creator1))685 CreatorClaim(cross(cur), id)686 }687 fees := FeesAccrued()688 uassert.True(t, fees > 0)689690 collectorBefore := balanceOf(FeeCollector)691 testing.SetRealm(testing.NewUserRealm(mallory)) // anyone may trigger692 got := WithdrawFees(cross(cur))693 uassert.Equal(t, fees, got)694 uassert.Equal(t, collectorBefore+fees, balanceOf(FeeCollector))695 uassert.Equal(t, int64(0), FeesAccrued())696697 testing.SetRealm(testing.NewUserRealm(mallory))698 uassert.AbortsWithMessage(t, cur, "no fees accrued", func() {699 WithdrawFees(cross(cur))700 })701}702703func TestSweepSurplus(cur realm, t *testing.T) {704 begin()705 // No surplus: coinio refuses.706 testing.SetRealm(testing.NewUserRealm(mallory))707 uassert.AbortsWithMessage(t, cur, "coinio: no surplus to sweep for ugnot", func() {708 SweepSurplus(cross(cur), Denom)709 })710711 // Force-send 33_000 ugnot out-of-band, then sweep it.712 testing.IssueCoins(Address(), ugnot(33_000))713 liabBefore := Liabilities()714 collectorBefore := balanceOf(FeeCollector)715 testing.SetRealm(testing.NewUserRealm(mallory))716 swept := SweepSurplus(cross(cur), Denom)717 uassert.Equal(t, int64(33_000), swept)718 uassert.Equal(t, collectorBefore+33_000, balanceOf(FeeCollector))719 uassert.Equal(t, liabBefore, Liabilities()) // tracked money untouched720721 // A foreign denomination sweeps in full.722 testing.IssueCoins(Address(), chain.NewCoins(chain.NewCoin("foocoin", 4_200)))723 testing.SetRealm(testing.NewUserRealm(mallory))724 uassert.Equal(t, int64(4_200), SweepSurplus(cross(cur), "foocoin"))725}726727// --- stray sends ---728729func TestNonPayableEntrypointsRejectCoins(cur realm, t *testing.T) {730 begin()731 id := launchAs(cur, creator1, "stray send case")732733 cases := []func(){734 func() { Launch(cross(cur), "t", "", MinGoal, MinDurationBlocks) },735 func() { Unpledge(cross(cur), id) },736 func() { CreatorClaim(cross(cur), id) },737 func() { CreatorCancel(cross(cur), id) },738 func() { SettleFailed(cross(cur), id) },739 func() { Lapse(cross(cur), id) },740 func() { Refund(cross(cur), id) },741 func() { Prune(cross(cur), id) },742 func() { WithdrawFees(cross(cur)) },743 func() { SweepSurplus(cross(cur), Denom) },744 }745 for _, call := range cases {746 testing.SetRealm(testing.NewUserRealm(mallory))747 testing.SetOriginSend(ugnot(1))748 uassert.AbortsWithMessage(t, cur, "this entrypoint does not accept coins", call)749 }750 testing.SetOriginSend(nil)751 testing.SetRealm(testing.NewUserRealm(creator1))752 CreatorCancel(cross(cur), id)753}754755// --- render ---756757func TestRenderIndexAndCampaign(cur realm, t *testing.T) {758 begin()759 // A hostile title must come out of the sanitizer defanged.760 hostile := "[evil](https://x) <script>alert(1)</script>"761 testing.SetRealm(testing.NewUserRealm(creator1))762 id := Launch(cross(cur), hostile, "memo with [link](x) inside", MinGoal, MinDurationBlocks)763764 index := Render("")765 uassert.True(t, strings.Contains(index, "# Crowdfund"))766 uassert.True(t, strings.Contains(index, "conservation: OK"))767 uassert.False(t, strings.Contains(index, "<script>"))768769 page := Render(itoa(id))770 uassert.True(t, strings.Contains(page, "status: **funding**"))771 uassert.True(t, strings.Contains(page, "0%"))772 uassert.False(t, strings.Contains(page, "<script>"))773 uassert.False(t, strings.Contains(page, "[evil](https://x)"))774 uassert.False(t, strings.Contains(page, "[link](x)"))775776 uassert.True(t, strings.Contains(Render("notanumber"), "invalid campaign id"))777 uassert.True(t, strings.Contains(Render("999999"), "unknown campaign id"))778779 testing.SetRealm(testing.NewUserRealm(creator1))780 CreatorCancel(cross(cur), id)781}782783func TestPercentAndBps(t *testing.T) {784 uassert.Equal(t, "0%", percent(0, MinGoal))785 uassert.Equal(t, "50%", percent(500_000, MinGoal))786 uassert.Equal(t, "99%", percent(999_999, MinGoal))787 uassert.Equal(t, "100%", percent(MinGoal, MinGoal))788 uassert.Equal(t, "250%", percent(2_500_000, MinGoal))789 // The overflow-guard branch: r*100 would not fit in int64. Three790 // quarters of 2^62 is exactly 75%, and the scaled form lands on it.791 huge := int64(1) << 62792 uassert.Equal(t, "75%", percent(3*(huge/4), huge))793794 uassert.Equal(t, "1%", bps(100))795 uassert.Equal(t, "5%", bps(500))796 uassert.Equal(t, "0.5%", bps(50))797 uassert.Equal(t, "0.01%", bps(1))798 uassert.Equal(t, "2.25%", bps(225))799}800Address() string
CampaignInfo(id int64) (creator string, goal int64, raised int64, feeBps int64, createdAt int64, deadline int64, claimEnd int64, backers int, state string)
CampaignMemo(id int64) string
CampaignTitle(id int64) string
CreatorCancel(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id int64)
CreatorClaim(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id int64)
FeesAccrued() int64
Height() int64
Held() int64
IsRefundable(id int64) bool
Lapse(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id int64)
Launch(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, title string, memo string, goal int64, durationBlocks int64) int64
Liabilities() int64
NumCampaigns() int64
OpenCampaigns() int64
OpenOf(addr string) int64
Pledge(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id int64)
PledgeOf(id int64, addr string) int64
Prune(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id int64)
Refund(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id int64)
Render(path string) string
SettleFailed(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id int64)
Status(id int64) string
Surplus() int64
SweepSurplus(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, denom string) int64
TotalBacked() int64
Unpledge(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, id int64)
WithdrawFees(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}) int64
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.