Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

payout.gno

8.54 Kb · 248 lines
  1package bolo
  2
  3import (
  4	"chain"
  5	"chain/banker"
  6	"chain/runtime"
  7	"math/bits"
  8
  9	"gno.land/p/nt/seqid/v0"
 10)
 11
 12// The refund windows' names: Refund's kind, GetContribution's window (API §3.2).
 13const (
 14	windowOpen = "open"
 15	windowExit = "exit"
 16)
 17
 18// Withdraw sends everything campaign id's escrow holds to its beneficiary, and returns
 19// how much: the beneficiary only, until the exit window opens — the campaign's page
 20// says when.
 21//
 22// The beneficiary only — a user by a direct transaction, or a deployed realm, through
 23// C11's realm branch (API §3.3). The tracked part counts as Withdrawn,
 24// and only it ends the open refund window; coins sent to the escrow outside Bolo go
 25// along, recorded apart.
 26func Withdraw(cur realm, id string) int64 {
 27	assertCurrent(cur)
 28	if cur.Previous().IsUserCall() {
 29		runtime.AssertOriginCall()
 30	} else {
 31		assertCodeCaller(cur)
 32	}
 33	c := campaignOf(id)
 34	recordDeadline(cur, c)
 35	if cur.Previous().Address() != c.beneficiary {
 36		panic(ErrNotBeneficiary)
 37	}
 38	w := bookWithdrawal(c)
 39	total := w.amount + w.untracked
 40	banker.NewBanker(banker.BankerTypeRealmSend, escrowSub(cur, c)).SendCoins(escrowAddr(c.id), c.beneficiary, chain.NewCoins(chain.NewCoin("ugnot", total)))
 41	chain.Emit(evWithdrawn,
 42		"id", idString(c.id),
 43		"seq", idString(seqid.ID(w.seq)),
 44		"to", w.to.String(),
 45		"amount", itoa(w.amount),
 46		"untracked", itoa(w.untracked),
 47	)
 48	return total
 49}
 50
 51// bookWithdrawal checks a withdrawal from c and enters it in the books, before any
 52// coin moves (API §3.3): the refusals of checkWithdrawable; nothing at the escrow;
 53// INV-1. Everything at the escrow goes, the tracked part as Withdrawn — ending the
 54// open refund window only when above 0 — and the rest recorded apart. It returns the
 55// stored record.
 56func bookWithdrawal(c *campaign) *withdrawal {
 57	checkWithdrawable(c)
 58	total := banker.NewReadonlyBanker().GetCoin(escrowAddr(c.id), "ugnot")
 59	if total == 0 {
 60		panic(ErrNothingToWithdraw)
 61	}
 62	assertBooks(c)
 63	s := c.state
 64	tracked := trackedBalance(c)
 65	s.withdrawn += tracked
 66	if tracked > 0 {
 67		s.everWithdrawn = true
 68	}
 69	seq := s.withdrawalSeq.Next()
 70	w := &withdrawal{seq: uint64(seq), amount: tracked, untracked: total - tracked, at: now(), to: c.beneficiary}
 71	c.withdrawals.Set(seq.Binary(), w)
 72	return w
 73}
 74
 75// checkWithdrawable refuses a withdrawal from c (API §3.3), in this order: a direct
 76// campaign, a settled one, and a closed one whose exit window has opened — what is
 77// left belongs to the exit process then.
 78func checkWithdrawable(c *campaign) {
 79	if c.mode != modeEscrow {
 80		panic(ErrWrongMode)
 81	}
 82	s := c.state
 83	if s.status == statusSettled {
 84		panic(ErrCampaignSettled)
 85	}
 86	if s.status == statusClosed && now() >= s.closedAt+c.params.ExitDelay {
 87		panic(ErrExitOpen)
 88	}
 89}
 90
 91// Refund gives you back, from campaign id's escrow, what you can take back now, and
 92// returns how much: your whole gift until the beneficiary first withdraws, or, once
 93// the beneficiary's window after the campaign closes has passed, your share of what
 94// they left uncollected. The campaign's page offers it when a donor can.
 95//
 96// It pays the caller back from their own contribution to the campaign (API §3.2,
 97// INV-13): a donor, by a direct transaction, while a refund window is open to them.
 98// The open window — no tracked withdrawal ever — pays everything not yet refunded.
 99func Refund(cur realm, id string) int64 {
100	assertCurrent(cur)
101	if !cur.Previous().IsUserCall() {
102		panic(ErrNotUserCall)
103	}
104	runtime.AssertOriginCall()
105	c := campaignOf(id)
106	recordDeadline(cur, c)
107	donor := cur.Previous().Address()
108	r := bookRefund(c, donor)
109	banker.NewBanker(banker.BankerTypeRealmSend, escrowSub(cur, c)).SendCoins(escrowAddr(c.id), donor, chain.NewCoins(chain.NewCoin("ugnot", r.amount)))
110	chain.Emit(evRefunded,
111		"id", idString(c.id),
112		"seq", idString(seqid.ID(r.seq)),
113		"donor", donor.String(),
114		"amount", itoa(r.amount),
115		"kind", r.kind,
116	)
117	return r.amount
118}
119
120// bookRefund checks a refund of donor's contribution to c and enters it in the
121// books, before any coin moves (API §3.2): the refusals of checkRefund, then INV-1,
122// on the books as they stand. An exit claim freezes the pool and its base if it is
123// the first, uses the donor's one claim, and may not exceed the tracked balance
124// (INV-17). It returns the stored record.
125func bookRefund(c *campaign, donor address) *refund {
126	k, window, amount := checkRefund(c, donor)
127	assertBooks(c)
128	s := c.state
129	if window == windowExit {
130		if !s.exitFrozen {
131			s.exitFrozen, s.exitPool, s.exitBase = true, trackedBalance(c), s.raised-s.refunded
132		}
133		if amount > trackedBalance(c) {
134			panic(ErrBooksMismatch)
135		}
136		k.exitClaimed = true
137	}
138	k.refunded += amount
139	s.refunded += amount
140	s.refundCount++
141	seq := s.refundSeq.Next()
142	r := &refund{seq: uint64(seq), amount: amount, at: now(), donor: donor, kind: window}
143	c.refunds.Set(seq.Binary(), r)
144	return r
145}
146
147// checkRefund returns donor's contribution to c, the window Refund would use and what
148// it would pay, or panics (API §3.2), in this order: a direct campaign; nothing to
149// refund; no window open, a used exit claim included; an exit claim that rounds down
150// to 0.
151func checkRefund(c *campaign, donor address) (*contribution, string, int64) {
152	if c.mode != modeEscrow {
153		panic(ErrWrongMode)
154	}
155	k := contributionOf(c, donor)
156	if k == nil || k.amount-k.refunded <= 0 {
157		panic(ErrNothingToRefund)
158	}
159	window, amount := refundable(c, k)
160	if window == "" {
161		panic(ErrRefundWindowClosed)
162	}
163	if amount == 0 {
164		panic(ErrNothingToRefund)
165	}
166	return k, window, amount
167}
168
169// refundable returns the window Refund would use for k now, and what it would pay
170// (API §3.2). The open window, while no tracked withdrawal ever happened, pays all not
171// yet refunded. After one, the exit window opens once the campaign is closed and
172// ExitDelay has passed, while a tracked balance is left: one claim per donor, pro-rata
173// against the pool and base the first claim froze — or would freeze now. It reads the
174// effective status, as GetContribution can't record a passed deadline.
175func refundable(c *campaign, k *contribution) (string, int64) {
176	left := k.amount - k.refunded
177	if c.mode != modeEscrow || left <= 0 {
178		return "", 0
179	}
180	s := c.state
181	if !s.everWithdrawn {
182		return windowOpen, left
183	}
184	status, _, closedAt := effective(c)
185	if status == statusOpen || now() < closedAt+c.params.ExitDelay || k.exitClaimed || trackedBalance(c) <= 0 {
186		return "", 0
187	}
188	pool, base := s.exitPool, s.exitBase
189	if !s.exitFrozen {
190		pool, base = trackedBalance(c), s.raised-s.refunded
191	}
192	return windowExit, exitClaim(left, pool, base)
193}
194
195// exitClaim is left × pool / base, rounded down (API §3.2): computed in 128 bits, as
196// both factors fit in 63 bits but their product doesn't. A claimant's left is part of
197// the base, so the base is above 0 and the quotient at most the pool, and Div64's own
198// panics, keyless, can't be reached; both conditions are asserted first.
199func exitClaim(left, pool, base int64) int64 {
200	if base <= 0 || left > base || pool < 0 {
201		panic(ErrBooksMismatch)
202	}
203	hi, lo := bits.Mul64(uint64(left), uint64(pool))
204	quo, _ := bits.Div64(hi, lo, uint64(base))
205	return int64(quo)
206}
207
208// assertBooks panics with ErrBooksMismatch when c's books hold more than its escrow
209// does (INV-1). It runs before every payout from an escrow, on the books as they
210// stand, so a bug in the books stops a payment instead of paying out coins they
211// can't account for.
212func assertBooks(c *campaign) {
213	if banker.NewReadonlyBanker().GetCoin(escrowAddr(c.id), "ugnot") < trackedBalance(c) {
214		panic(ErrBooksMismatch)
215	}
216}
217
218// contributionOf returns donor's record in c, or nil.
219func contributionOf(c *campaign, donor address) *contribution {
220	if v := c.contributions.Get(donor.String()); v != nil {
221		return v.(*contribution)
222	}
223	return nil
224}
225
226// ContributionView is one donor's record in one campaign (API §2.4), with what Refund
227// would pay them now: a copy.
228type ContributionView struct {
229	Amount, Refunded, RefundableNow int64
230	Window                          string // "open", "exit", or "" for none: the window Refund would use now
231	ExitClaimed                     bool
232}
233
234// GetContribution returns donor's record in campaign id (API §4): zeros and no
235// window for an address that never gave, and no window for an exit claim that rounds
236// to 0, which Refund refuses. An address is compared as given (C12).
237func GetContribution(id string, donor address) ContributionView {
238	c := campaignOf(id)
239	k := contributionOf(c, donor)
240	if k == nil {
241		return ContributionView{}
242	}
243	window, pay := refundable(c, k)
244	if pay == 0 {
245		window = ""
246	}
247	return ContributionView{Amount: k.amount, Refunded: k.refunded, RefundableNow: pay, Window: window, ExitClaimed: k.exitClaimed}
248}