PathrockNetwork Gno Explorer
HomeBlocksTransactionsTokensRealmsPackagesValidatorsAnalytics

PathrockNetwork Gno Explorer — an independent explorer for Gno.land Mainnet (gnoland-1), operated by PathrockNetwork. Not an official Gno.land service.

gnowebarchive RPC

gno.land/r/nym-thegnomic001/gnomic_airdrop

Realm
Open in gnoweb ↗

Overview

Kind
Realm (renderable)
Name
gnomic_airdrop
Namespace
nym-thegnomic001
Files
3 (gnomod.toml)
Exported functions
46
Module
gno.land/r/nym-thegnomic001/gnomic_airdrop
gno
0.9

Files (3)

  • gnomod.tomltoml
  • airdrop.gnogno
  • config.gnogno
airdrop.gnogno
1// Claim-based airdrop realm with Merkle proofs.2//3// Why Merkle rather than an on-chain list:4//   - the beneficiary list can run to tens of thousands of rows; writing it5//     on-chain costs gas and storage deposit in proportion;6//   - here only 32 bytes go on-chain (the root), plus one record per ACTUAL7//     claim. Whoever never claims costs nothing.8//9// Operating flow:10//  1. deploy this realm;11//  2. fund it by transferring GRC20 tokens to it (its address is returned by12//     RealmAddress());13//  3. the admin publishes root, leaf count and deadline block with SetCampaign;14//  4. each beneficiary calls Claim with their own proof;15//  5. after the deadline the admin recovers the residue with Sweep.16//17// The Merkle leaf is the string "<address>|<amount in base units>", UTF-818// encoded. The tree is Tendermint's "simple tree" (gno's crypto/merkle):19// leaf = SHA256(0x00||data), node = SHA256(0x01||left||right), split at the20// largest power of two below n. The generator in tools/merkle produces21// compatible proofs.22package gnomic_airdrop2324import (25	"chain"26	"math"27	"chain/runtime"28	"chain/runtime/unsafe"29	"crypto/merkle"30	"encoding/hex"31	"strconv"32	"strings"33	"time"3435	"gno.land/p/nt/avl/v0"36	"gno.land/p/nt/ownable/v0"37	"gno.land/p/nt/ufmt/v0"38	"gno.land/r/nt/grc20reg/v0"3940	// Realm of the distributed token. Transfers go through the registry to stay41	// decoupled, but BURNING needs the real realm: the registry exposes42	// Transfer/Approve/TransferFrom, not Burn. scripts/render.sh rewrites this43	// path with the actual token's one.44	token "gno.land/r/nym-thegnomic001/gnomic"45)4647// Ownable holds the administrative authority over the campaign.48var Ownable *ownable.Ownable4950// tokenKey is the key of the distributed token. It is a variable rather than a51// constant only so that tests can register one of their own; in production it52// keeps the config.gno value for the realm's whole life.53var tokenKey = defaultTokenKey5455// claimWindowSeconds is a variable for the same reason: tests shrink or widen56// it. In production it keeps the config.gno value for the realm's whole life.57var claimWindowSeconds = defaultClaimWindowSeconds5859// withdrawCooldownSeconds likewise: tests switch it off, production keeps it.60var withdrawCooldownSeconds = defaultWithdrawCooldownSeconds6162var (63	root      []byte // Merkle root of the current campaign64	leafCount int    // total number of leaves (needed to verify)65	endHeight int64  // block past which no one can claim (0 = no limit)66	epoch     int    // bumped on every new campaign6768	// claimed is the register of grants: "<address>" -> *grant.69	//70	// It is also the ONLY defence against double claiming. A Merkle proof shows71	// you are entitled, but it is reusable forever: only this register knows72	// that you have already collected.73	//74	// CAREFUL: never delete an entry, not even a settled one to reclaim its75	// storage deposit. It looks like a harmless optimisation and instead76	// reopens double claiming: someone who already withdrew everything would77	// present the same proof to a register that has forgotten them.78	claimed avl.Tree7980	// campaignStart is when the current campaign opened: linear release runs81	// from there for EVERYONE, not from the moment of the individual claim.82	// Otherwise a late claimer would vest late, and there would be a reason to83	// rush to claim just to start the clock sooner.84	//85	// Every grant keeps a copy in grant.start: opening a new campaign must not86	// disturb the vesting already under way for whoever claimed in the87	// previous one.88	campaignStart int648990	// outstanding is the sum of what has been granted but not yet withdrawn.91	// It protects beneficiaries: Sweep cannot touch this part.92	outstanding int649394	// sealed makes opening further campaigns impossible. It is the equivalent95	// of DropOwnership on the token: a "one-off" airdrop announced in words96	// stays a promise; sealed in code it becomes a property anyone can verify97	// with a query.98	sealed bool99100	totalClaimed  int64 // total actually paid out101	totalGranted  int64 // total granted (including what has not vested yet)102	totalBurned   int64 // total destroyed: instant exits, renunciations, settlement103	settledBurned int64 // burned by SettleUnclaimed104	settledToFund int64 // sent to the ecosystem fund by SettleUnclaimed105	totalToFund   int64 // sent to the ecosystem fund by voluntary forfeits106	claimCount    int107	realmAddr     address108)109110// grant is the entitlement a beneficiary obtained in a campaign.111// It always holds that total == withdrawn + burned + toFund + (not yet released).112//113// Grants are keyed by address ALONE, not by "epoch:address": tying the key to114// the epoch made the previous campaign's grants unreachable as soon as a new115// one opened — the beneficiary could no longer withdraw and the tokens stayed116// counted in outstanding, so not even Sweep could recover them.117type grant struct {118	epoch        int   // campaign it was granted in119	start        int64 // start of this grant's vesting120	total        int64 // total amount granted121	withdrawn    int64 // how much has already been transferred122	burned       int64 // how much was destroyed (instant exit or forfeit)123	toFund       int64 // how much went to the ecosystem fund (forfeit)124	lastWithdraw int64 // unix time of the last payout; drives the cooldown125}126127// settled reports whether nothing is left to pay out on the grant.128func (g *grant) settled() bool { return g.total <= g.withdrawn+g.burned+g.toFund }129130// closed reports whether the grant was ended by an instant exit or a forfeit.131func (g *grant) closed() bool { return g.burned > 0 || g.toFund > 0 }132133const (134	SealEvent     = "AirdropSeal"135	SettleEvent   = "AirdropSettleUnclaimed"136	ClaimEvent    = "AirdropClaim"137	WithdrawEvent = "AirdropWithdraw"138	ForfeitEvent  = "AirdropForfeit"139	CampaignEvent = "AirdropCampaign"140)141142const bpsDenominator = 10000143144func init(cur realm) {145	admin := address(adminAddress)146	if !admin.IsValid() {147		admin = cur.Previous().Address()148	}149	if !admin.IsValid() {150		admin = unsafe.OriginCaller()151	}152	if !admin.IsValid() {153		panic("airdrop: cannot determine the administrator")154	}155	Ownable = ownable.NewWithAddress(admin)156	realmAddr = cur.Address()157158	// The registry key and the imported realm must name the SAME token.159	// Transfers go through the registry (by key) while burns call the imported160	// realm directly: if the two ever disagreed the airdrop would pay out one161	// token and destroy another, and nothing would notice until the first162	// forfeit. Deriving the key from the import makes them equal by163	// construction; the constant survives only to make a wrong TOKEN_KEY in164	// config.env fail here, at deploy, instead of silently in a year.165	tokenKey = token.TokenKey()166	if defaultTokenKey != tokenKey {167		panic("airdrop: config.gno names " + defaultTokenKey +168			" but the imported token realm is " + tokenKey)169	}170}171172// ---------------------------------------------------------------------------173// Administration174// ---------------------------------------------------------------------------175176// SetCampaign publishes a new campaign: rootHex is the Merkle root in hex177// (32 bytes), total the number of leaves in the tree, end the deadline block178// (0 = no deadline).179//180// Publishing a new campaign resets the claim history: whoever appears on both181// lists can claim again. That is deliberate — it allows recurring airdrops on182// the same realm without a redeploy.183func SetCampaign(cur realm, rootHex string, total int, end int64) {184	Ownable.AssertOwnedBy(cur.Previous().Address())185	if sealed {186		panic("airdrop: campaign is sealed, no further campaign can be opened")187	}188189	raw, err := hex.DecodeString(strings.TrimPrefix(rootHex, "0x"))190	if err != nil {191		panic("airdrop: invalid root: " + err.Error())192	}193	if len(raw) != 32 {194		panic("airdrop: the root must be 32 bytes")195	}196	if total <= 0 {197		panic("airdrop: the leaf count must be positive")198	}199	if end != 0 && end <= runtime.ChainHeight() {200		panic("airdrop: the deadline must be in the future")201	}202203	root = raw204	leafCount = total205	endHeight = end206	epoch++207	campaignStart = time.Now().Unix()208209	chain.Emit(210		CampaignEvent,211		"epoch", strconv.Itoa(epoch),212		"root", hex.EncodeToString(raw),213		"leaves", strconv.Itoa(total),214		"end", strconv.FormatInt(end, 10),215	)216}217218// CloseCampaign stops claims immediately.219func CloseCampaign(cur realm) {220	Ownable.AssertOwnedBy(cur.Previous().Address())221	// Dopo il sigillo non si chiude piu' niente. Il sigillo esiste per dire222	// "questi termini sono definitivi", e lasciare in mano al proprietario un223	// interruttore che toglie a tutti la possibilita' di rivendicare lo224	// contraddiceva: bastava chiudere e aspettare i dodici mesi perche' meta'225	// del non rivendicato bruciasse e meta' finisse al fondo. Prima del226	// sigillo la chiusura serve — e' il modo di rimediare a una radice227	// sbagliata — dopo non ha piu' nessun uso legittimo.228	if sealed {229		panic("airdrop: the campaign is sealed, it cannot be closed")230	}231	root = nil232	leafCount = 0233	endHeight = 0234}235236// SealCampaign closes off the possibility of opening new campaigns forever.237// Irreversible: no code path sets sealed back to false. Call it once the238// published root is the final one.239//240// Until then the owner can fix a mistake by republishing the root; afterwards241// the airdrop is provably one-off and anyone can check with IsSealed().242func SealCampaign(cur realm) {243	Ownable.AssertOwnedBy(cur.Previous().Address())244	if sealed {245		panic("airdrop: already sealed")246	}247	if len(root) != 32 {248		panic("airdrop: no published root to seal")249	}250	sealed = true251	chain.Emit(SealEvent, "epoch", strconv.Itoa(epoch), "root", hex.EncodeToString(root))252}253254// SettleUnclaimed disposes of whatever was never claimed, once the claim255// window has elapsed: half is burned, the other half goes to the team fund.256//257// The rule is enforced here rather than promised: there is no Sweep and no258// BurnRemaining, so the owner cannot pocket the residue nor destroy all of it.259// Settlement waits for the window even if CloseCampaign stopped claims260// earlier, and grants already made stay untouchable — only the free part261// (balance minus outstanding) is settled. Anyone may call it: it is a public262// service, not a privilege.263func SettleUnclaimed(cur realm) {264	if campaignStart == 0 {265		panic("airdrop: no campaign was ever opened")266	}267	if time.Now().Unix() < campaignStart+claimWindowSeconds {268		panic("airdrop: the claim window has not elapsed yet")269	}270	if isOpen() {271		panic("airdrop: the campaign is still open")272	}273	free := Balance() - outstanding274	if free <= 0 {275		panic("airdrop: no unclaimed residue to settle")276	}277	team := address(ecosystemFundAddress)278	if !team.IsValid() {279		panic("airdrop: ecosystem fund address is not configured")280	}281	burn := free / 2282	toTeam := free - burn283	totalBurned = addSat(totalBurned, burn)284	settledBurned = addSat(settledBurned, burn)285	settledToFund = addSat(settledToFund, toTeam)286	// Le due guardie non sono pedanteria: con un residuo di UNA unita' base287	// burn vale zero, e il ledger rifiuta un rogo di zero. Senza il controllo288	// la liquidazione fallirebbe per sempre proprio sul residuo piu' probabile289	// di tutti, quello lasciato dal troncamento, e quell'unita' resterebbe290	// chiusa nel realm senza che nessuno possa piu' toccarla.291	if burn > 0 {292		token.Burn(cross(cur), burn)293	}294	if toTeam > 0 {295		grc20reg.Transfer(0, cur, tokenKey, team, toTeam)296	}297	chain.Emit(SettleEvent,298		"burned", strconv.FormatInt(burn, 10),299		"to_team", strconv.FormatInt(toTeam, 10),300		"team", team.String())301}302303// TransferOwnership hands administration of the campaign over.304func TransferOwnership(cur realm, newOwner address) {305	if err := Ownable.TransferOwnership(0, cur, newOwner); err != nil {306		panic(err.Error())307	}308}309310// ---------------------------------------------------------------------------311// Claiming312// ---------------------------------------------------------------------------313314// Claim pays amount tokens to the caller, if the Merkle proof315// (index, auntsHex) shows that "<caller>|<amount>" is a leaf of the published316// tree.317//318// auntsHex is the hex concatenation of the sibling hashes, 32 bytes each, just319// as produced by tools/merkle.320func Claim(cur realm, amount int64, index int, auntsHex string) {321	caller := cur.Previous().Address()322	if !cur.Previous().IsUserCall() {323		panic("airdrop: a claim must come from a user account")324	}325	claimFor(cur, caller, amount, index, auntsHex)326}327328// claimFor is the body of Claim. It is unexported on purpose: a transaction329// can only reach exported functions, so from outside the realm the only way in330// is Claim, which always uses the caller's own address. The tests, being in331// the same package, still exercise the registration logic directly.332func claimFor(cur realm, beneficiary address, amount int64, index int, auntsHex string) {333	registerGrant(beneficiary, amount, index, auntsHex)334	releaseIfAny(cur, beneficiary)335}336337// There is deliberately no "claim on behalf of" entry point.338//339// It used to exist, so that a third party could pay the gas for someone: the340// tokens went to the beneficiary either way, so it looked harmless. It is not.341// Registering the grant is what SPENDS the one choice this airdrop allows: the342// beneficiary can no longer take the instant 30%, nor renounce, because the343// vesting path has already been picked for them. A valid Merkle proof proves344// entitlement, never consent, and every proof is public.345//346// WithdrawFor stays: it only moves what has already vested to its owner, and347// takes no decision away from anyone.348349// ClaimInstant is the first of the two alternatives to Claim: the instant350// share (instantBps, 30%) is paid at once and the rest (70%) is given up the351// same way a forfeit is — half burned, half to the ecosystem fund — whenever352// it is chosen, even after the whole grant would have vested. The share is353// larger than Claim's immediate 10% on purpose: it makes the shortcut a real354// choice rather than a punishment. It cannot be undone355// and suits someone who prefers a certain fraction today to the whole a year356// from now. The residue is destroyed, to every holder's benefit.357//358// Only the person concerned can choose it: there is no "on behalf of" form.359func ClaimInstant(cur realm, amount int64, index int, auntsHex string) {360	if !cur.Previous().IsUserCall() {361		panic("airdrop: a claim must come from a user account")362	}363	beneficiary := cur.Previous().Address()364	registerGrant(beneficiary, amount, index, auntsHex)365	g := grantOf(beneficiary)366367	fund := address(ecosystemFundAddress)368	if !fund.IsValid() {369		panic("airdrop: ecosystem fund address is not configured")370	}371	immediate := shareOf(g.total, instantBps)372	rest := g.total - immediate373	burn := rest / 2374	toFund := rest - burn375	g.withdrawn = immediate376	g.burned = burn377	g.toFund = toFund378	g.lastWithdraw = time.Now().Unix()379	outstanding -= g.total380	totalClaimed = addSat(totalClaimed, immediate)381	totalBurned = addSat(totalBurned, burn)382	totalToFund = addSat(totalToFund, toFund)383384	if immediate > 0 {385		grc20reg.Transfer(0, cur, tokenKey, beneficiary, immediate)386	}387	if burn > 0 {388		// Burn acts on the caller's balance: this realm holds the escrow.389		token.Burn(cross(cur), burn)390	}391	if toFund > 0 {392		grc20reg.Transfer(0, cur, tokenKey, fund, toFund)393	}394	chain.Emit(WithdrawEvent, "epoch", strconv.Itoa(epoch), "to", beneficiary.String(),395		"amount", strconv.FormatInt(immediate, 10), "remaining", "0")396	chain.Emit(ForfeitEvent, "epoch", strconv.Itoa(epoch), "who", beneficiary.String(),397		"burned", strconv.FormatInt(burn, 10), "to_fund", strconv.FormatInt(toFund, 10))398}399400// Forfeit is the second alternative to Claim: the beneficiary gives the grant401// up without taking anything. Half is burned, half goes to the ecosystem fund —402// the same split SettleUnclaimed applies to whoever never acted at all.403//404// The proof is still required and the grant is still recorded: without that,405// the same address could forfeit and then claim. The record makes it final.406// Only the person concerned can choose it: there is no "on behalf of" form.407func Forfeit(cur realm, amount int64, index int, auntsHex string) {408	if !cur.Previous().IsUserCall() {409		panic("airdrop: a forfeit must come from a user account")410	}411	beneficiary := cur.Previous().Address()412	fund := address(ecosystemFundAddress)413	if !fund.IsValid() {414		panic("airdrop: ecosystem fund address is not configured")415	}416	registerGrant(beneficiary, amount, index, auntsHex)417	g := grantOf(beneficiary)418419	burn := g.total / 2420	toFund := g.total - burn421	g.burned = burn422	g.toFund = toFund423	outstanding -= g.total424	totalBurned = addSat(totalBurned, burn)425	totalToFund = addSat(totalToFund, toFund)426427	if burn > 0 {428		token.Burn(cross(cur), burn)429	}430	if toFund > 0 {431		grc20reg.Transfer(0, cur, tokenKey, fund, toFund)432	}433	chain.Emit(ForfeitEvent, "epoch", strconv.Itoa(epoch), "who", beneficiary.String(),434		"burned", strconv.FormatInt(burn, 10), "to_fund", strconv.FormatInt(toFund, 10))435}436437// immediateOf is the share Claim unlocks at once: immediateBps/10000 of total.438func immediateOf(total int64) int64 { return shareOf(total, immediateBps) }439440// shareOf is bps/10000 of total, in two steps to stay inside int64.441func shareOf(total int64, bps int) int64 {442	v := total/bpsDenominator*int64(bps) + total%bpsDenominator*int64(bps)/bpsDenominator443	if v > total {444		v = total445	}446	return v447}448449// registerGrant checks the proof and records the grant. It transfers nothing.450func registerGrant(beneficiary address, amount int64, index int, auntsHex string) {451	if !isOpen() {452		panic("airdrop: no open campaign")453	}454	if !beneficiary.IsValid() {455		panic("airdrop: invalid beneficiary")456	}457	if amount <= 0 {458		panic("airdrop: invalid amount")459	}460	if index < 0 || index >= leafCount {461		panic("airdrop: index out of range")462	}463464	key := beneficiary.String()465	if prev := grantOf(beneficiary); prev != nil {466		if prev.epoch == epoch {467			panic("airdrop: already claimed")468		}469		// A previous grant still vesting must not be overwritten: that would be470		// a silent loss for the beneficiary. It has to be closed first, with471		// Withdraw once vesting is over or with Forfeit.472		if !prev.settled() {473			panic("airdrop: an earlier grant is still running, close it first")474		}475	}476477	aunts, err := hex.DecodeString(strings.TrimPrefix(auntsHex, "0x"))478	if err != nil {479		panic("airdrop: invalid proof: " + err.Error())480	}481	if len(aunts)%32 != 0 {482		panic("airdrop: the proof must be a multiple of 32 bytes")483	}484485	leaf := []byte(Leaf(beneficiary, amount))486	if !merkle.VerifySimpleProof(root, leaf, index, leafCount, aunts) {487		panic("airdrop: invalid Merkle proof")488	}489490	// The realm must be able to cover the new grant ON TOP of those already made.491	if Balance()-outstanding < amount {492		panic("airdrop: insufficient funds in the realm")493	}494495	claimed.Set(key, &grant{epoch: epoch, start: campaignStart, total: amount})496	outstanding += amount497	totalGranted = addSat(totalGranted, amount)498	claimCount++499500	chain.Emit(501		ClaimEvent,502		"epoch", strconv.Itoa(epoch),503		"to", beneficiary.String(),504		"amount", strconv.FormatInt(amount, 10),505	)506}507508// Withdraw transfers to the caller the vested, not-yet-withdrawn part.509func Withdraw(cur realm) {510	release(cur, cur.Previous().Address())511}512513// WithdrawFor is like Withdraw but on another beneficiary's behalf: the tokens514// still go to beneficiary, whoever sends the transaction only pays the gas.515func WithdrawFor(cur realm, beneficiary address) {516	release(cur, beneficiary)517}518519// release transfers the vested share. It is the public form: when there is520// nothing to withdraw it says so, instead of silently letting through a521// transaction that did nothing.522func release(cur realm, beneficiary address) {523	if releaseIfAny(cur, beneficiary) == 0 {524		panic("airdrop: nothing to withdraw right now")525	}526}527528// releaseIfAny is the internal form: it transfers whatever has vested and529// returns the amount, or 0 if nothing has vested yet. It serves the paths where530// "nothing to withdraw" is normal rather than an error: a claim whose immediate531// share is zero, and a forfeit, whose point is to burn the residue rather than532// to collect.533// Payouts are rate-limited: one every withdrawCooldownSeconds per grant, the534// claim itself counting as the first. The very first payout is never delayed.535func releaseIfAny(cur realm, beneficiary address) int64 {536	g := grantOf(beneficiary)537	if g == nil {538		panic("airdrop: no grant for this address")539	}540	if g.closed() {541		panic("airdrop: grant closed by an instant exit or a forfeit")542	}543	amount := vestedOf(g) - g.withdrawn544	if amount <= 0 {545		return 0546	}547	now := time.Now().Unix()548	if g.lastWithdraw > 0 && now < g.lastWithdraw+withdrawCooldownSeconds {549		panic("airdrop: next withdrawal available in " +550			strconv.FormatInt(g.lastWithdraw+withdrawCooldownSeconds-now, 10) + " seconds")551	}552553	g.lastWithdraw = now554	g.withdrawn += amount555	outstanding -= amount556	totalClaimed = addSat(totalClaimed, amount)557558	grc20reg.Transfer(0, cur, tokenKey, beneficiary, amount)559560	chain.Emit(561		WithdrawEvent,562		"epoch", strconv.Itoa(epoch),563		"to", beneficiary.String(),564		"amount", strconv.FormatInt(amount, 10),565		"remaining", strconv.FormatInt(g.total-g.withdrawn-g.burned-g.toFund, 10),566	)567	return amount568}569570// vestedOf computes how much of a grant has vested:571//572//	vested = immediate + rest * elapsed / duration573//574// where immediate is immediateBps/10000 of the total. The division is done in575// two steps (quotient and remainder) because rest*elapsed would overflow int64576// for large amounts: 1e13 * 3.15e7 exceeds 9.2e18.577func vestedOf(g *grant) int64 {578	immediate := immediateOf(g.total)579	rest := g.total - immediate580	if rest == 0 || vestingSeconds <= 0 {581		return g.total582	}583584	elapsed := time.Now().Unix() - g.start585	if elapsed <= 0 {586		return immediate587	}588	if elapsed >= vestingSeconds {589		return g.total590	}591592	linear := rest/vestingSeconds*elapsed + rest%vestingSeconds*elapsed/vestingSeconds593	return immediate + linear594}595596// addSat adds without ever wrapping. These counters only grow, across every597// campaign, and nothing bounds them to the supply: a statistic that turns598// negative would be bad, one that blocks a withdrawal would be worse.599func addSat(a, b int64) int64 {600	if b > 0 && a > math.MaxInt64-b {601		return math.MaxInt64602	}603	return a + b604}605606func grantOf(addr address) *grant {607	v := claimed.Get(addr.String())608	if v == nil {609		return nil610	}611	return v.(*grant)612}613614// ---------------------------------------------------------------------------615// Reads616// ---------------------------------------------------------------------------617618// Leaf returns the canonical Merkle-leaf encoding for the (address, amount)619// pair. It must match byte for byte the one used by the off-chain generator.620func Leaf(addr address, amount int64) string {621	return addr.String() + "|" + strconv.FormatInt(amount, 10)622}623624// RealmAddress is the address to fund with the airdrop tokens.625func RealmAddress() address { return realmAddr }626627// Balance is the token balance the realm currently holds.628func Balance() int64 {629	token := grc20reg.Get(tokenKey)630	if token == nil {631		return 0632	}633	return token.BalanceOf(realmAddr)634}635636// Root returns the current Merkle root in hex.637func Root() string { return hex.EncodeToString(root) }638639// Epoch is the number of the current campaign.640func Epoch() int { return epoch }641642// IsSealed reports whether the campaign has been sealed: if true, no new643// campaign can ever be opened.644func IsSealed() bool { return sealed }645646// LeafCount is the number of leaves in the published tree.647func LeafCount() int { return leafCount }648649// EndHeight is the deadline block (0 = no deadline).650func EndHeight() int64 { return endHeight }651652// IsOpen reports whether claims are currently accepted.653func IsOpen() bool { return isOpen() }654655// HasClaimed reports whether addr has already claimed in the current campaign.656func HasClaimed(addr address) bool {657	g := grantOf(addr)658	return g != nil && g.epoch == epoch659}660661// HasGrant reports whether addr holds a grant, from this campaign or an662// earlier one.663func HasGrant(addr address) bool { return grantOf(addr) != nil }664665// GrantEpoch returns the campaign in which addr obtained its grant.666func GrantEpoch(addr address) int {667	g := grantOf(addr)668	if g == nil {669		return 0670	}671	return g.epoch672}673674// VestingStartOf returns when vesting starts for addr's grant.675func VestingStartOf(addr address) int64 {676	g := grantOf(addr)677	if g == nil {678		return 0679	}680	return g.start681}682683// ClaimedAmount returns addr's total grant, vested or not.684func ClaimedAmount(addr address) int64 {685	g := grantOf(addr)686	if g == nil {687		return 0688	}689	return g.total690}691692// Vested returns how much of addr's grant has vested so far. A closed grant693// (instant exit or forfeit) will never vest anything more: it reports what694// was paid, not where the curve would be.695func Vested(addr address) int64 {696	g := grantOf(addr)697	if g == nil {698		return 0699	}700	if g.closed() {701		return g.withdrawn702	}703	return vestedOf(g)704}705706// InstantBps is the share ClaimInstant pays at once, in hundredths of a percent.707func InstantBps() int { return instantBps }708709// Withdrawn returns how much addr has already withdrawn.710func Withdrawn(addr address) int64 {711	g := grantOf(addr)712	if g == nil {713		return 0714	}715	return g.withdrawn716}717718// Withdrawable returns how much addr can withdraw right now.719func Withdrawable(addr address) int64 {720	g := grantOf(addr)721	if g == nil || g.closed() {722		return 0723	}724	// During the cooldown the answer is zero, not "what has vested": this725	// function says what Withdraw would pay right now, and right now it would726	// refuse. NextWithdrawAt says when that changes.727	if g.lastWithdraw > 0 && time.Now().Unix() < g.lastWithdraw+withdrawCooldownSeconds {728		return 0729	}730	return vestedOf(g) - g.withdrawn731}732733// NextWithdrawAt returns the unix time from which addr may withdraw again734// (0 when there is no grant, the grant is closed, or no cooldown is running).735func NextWithdrawAt(addr address) int64 {736	g := grantOf(addr)737	if g == nil || g.closed() || g.lastWithdraw == 0 {738		return 0739	}740	return g.lastWithdraw + withdrawCooldownSeconds741}742743// WithdrawCooldown is the minimum interval between two payouts, in seconds.744func WithdrawCooldown() int64 { return withdrawCooldownSeconds }745746// BurnedOf returns how much of addr's grant was destroyed.747func BurnedOf(addr address) int64 {748	g := grantOf(addr)749	if g == nil {750		return 0751	}752	return g.burned753}754755// FundOf returns how much of addr's grant went to the ecosystem fund.756func FundOf(addr address) int64 {757	g := grantOf(addr)758	if g == nil {759		return 0760	}761	return g.toFund762}763764// TotalToFund is what voluntary forfeits sent to the ecosystem fund.765func TotalToFund() int64 { return totalToFund }766767// TotalBurned is the total destroyed: instant exits, renunciations and the768// settlement of the unclaimed residue.769func TotalBurned() int64 { return totalBurned }770771// ClaimWindowEnd is the moment (unix) claims stop being accepted for the772// current campaign; SettleUnclaimed becomes callable from then on.773func ClaimWindowEnd() int64 { return campaignStart + claimWindowSeconds }774775// EcosystemFund is where the non-burned half of every forfeit goes.776func EcosystemFund() address { return address(ecosystemFundAddress) }777778// SettledBurned and SettledToFund report what SettleUnclaimed did.779func SettledBurned() int64 { return settledBurned }780func SettledToFund() int64 { return settledToFund }781782// Outstanding is the total granted and not yet withdrawn: the part of the783// realm's balance that Sweep cannot touch.784func Outstanding() int64 { return outstanding }785786// TotalGranted is the total of all grants made, vested or not.787func TotalGranted() int64 { return totalGranted }788789// VestingStart is the moment (unix) the CURRENT campaign's linear release runs790// from. Grants from earlier campaigns keep their own, readable with791// VestingStartOf.792func VestingStart() int64 { return campaignStart }793794// VestingEnd is the moment (unix) at which everything granted in the current795// campaign counts as fully vested.796func VestingEnd() int64 { return campaignStart + vestingSeconds }797798// ImmediateBps is the share unlocked at once, in hundredths of a percent.799func ImmediateBps() int { return immediateBps }800801// TotalClaimed is the total actually transferred to beneficiaries.802func TotalClaimed() int64 { return totalClaimed }803804// ClaimCount is the number of successful claims.805func ClaimCount() int { return claimCount }806807func isOpen() bool {808	if len(root) != 32 || leafCount == 0 {809		return false810	}811	if endHeight != 0 && runtime.ChainHeight() > endHeight {812		return false813	}814	// The claim window is part of the deal: after it, no claim is accepted and815	// the residue is settled by SettleUnclaimed.816	if claimWindowSeconds > 0 && time.Now().Unix() >= campaignStart+claimWindowSeconds {817		return false818	}819	return true820}821822func Render(path string) string {823	parts := strings.Split(path, "/")824	if len(parts) == 2 && parts[0] == "claimed" {825		addr := address(parts[1])826		if !addr.IsValid() {827			return "invalid address\n"828		}829		g := grantOf(addr)830		if g == nil {831			return "no grant for this address\n"832		}833		if g.closed() {834			return ufmt.Sprintf(835				"granted: %d\nwithdrawn: %d\nburned: %d\nto fund: %d\nstatus: closed\n",836				g.total, g.withdrawn, g.burned, g.toFund)837		}838		return ufmt.Sprintf(839			"granted: %d\nvested: %d\nwithdrawn: %d\navailable now: %d\n",840			g.total, vestedOf(g), g.withdrawn, vestedOf(g)-g.withdrawn)841	}842	if path != "" {843		return "404\n"844	}845846	s := ufmt.Sprintf("# %s\n\n", campaignName)847	s += "| | |\n|---|---|\n"848	s += ufmt.Sprintf("| Token | `%s` |\n", tokenKey)849	s += ufmt.Sprintf("| Realm address | `%s` |\n", realmAddr.String())850	s += ufmt.Sprintf("| Available balance | %d |\n", Balance())851	s += ufmt.Sprintf("| Campaign | #%d |\n", epoch)852	s += ufmt.Sprintf("| Status | %s |\n", statusLabel())853	s += ufmt.Sprintf("| Sealed | %s |\n", sealLabel())854	s += ufmt.Sprintf("| Merkle root | `%s` |\n", Root())855	s += ufmt.Sprintf("| Leaves | %d |\n", leafCount)856	s += ufmt.Sprintf("| Deadline (block) | %d |\n", endHeight)857	s += ufmt.Sprintf("| Claims | %d |\n", claimCount)858	s += ufmt.Sprintf("| Granted | %d |\n", totalGranted)859	s += ufmt.Sprintf("| Total paid out | %d |\n", totalClaimed)860	s += ufmt.Sprintf("| Burned | %d |\n", totalBurned)861	s += ufmt.Sprintf("| To the ecosystem fund (forfeits) | %d |\n", totalToFund)862	s += ufmt.Sprintf("| Still owed | %d |\n", outstanding)863	s += ufmt.Sprintf("| Current block | %d |\n", runtime.ChainHeight())864	s += "\n## Release\n\n"865	s += ufmt.Sprintf("- Unlocked immediately on claim: **%d%%**\n", immediateBps/100)866	s += ufmt.Sprintf("- The remaining **%d%%** vests linearly over %d days\n",867		100-immediateBps/100, vestingSeconds/86400)868	s += ufmt.Sprintf("- Withdrawals: at most one every %d hours per address, the claim counting as the first\n", withdrawCooldownSeconds/3600)869	s += ufmt.Sprintf("- Alternatively `ClaimInstant` pays exactly %d%% at once; of the other %d%%, half is **burned** and half goes to the ecosystem fund\n",870		instantBps/100, 100-instantBps/100)871	s += "- Or `Forfeit`: nothing paid, half burned, half to the ecosystem fund\n"872	s += ufmt.Sprintf("- Claims close %d days after opening; what was never claimed is then settled: **half burned, half to the ecosystem fund** (`%s`)\n",873		claimWindowSeconds/86400, ecosystemFundAddress)874	if settledBurned > 0 || settledToFund > 0 {875		s += ufmt.Sprintf("- Settled: %d burned, %d to the ecosystem fund\n", settledBurned, settledToFund)876	}877	if campaignStart > 0 {878		s += ufmt.Sprintf("- Start: %d, end: %d (unix)\n", campaignStart, VestingEnd())879		elapsed := time.Now().Unix() - campaignStart880		pct := int64(100)881		if elapsed < vestingSeconds {882			pct = elapsed * 100 / vestingSeconds883		}884		if pct < 0 {885			pct = 0886		}887		s += ufmt.Sprintf("- Progress: **%d%%**\n", pct)888	}889	s += "\nStatus of an address: `:claimed/<address>`\n"890	return s891}892893// sealLabel describes in Render whether the airdrop is provably one-off.894func sealLabel() string {895	if sealed {896		return "**yes, one-off**: no new campaign is possible any more"897	}898	return "no: the owner can still republish the root"899}900901func statusLabel() string {902	if isOpen() {903		return "**open**"904	}905	if len(root) == 32 {906		return "expired"907	}908	return "closed"909}910

Functions

  • Balance() int64

  • BurnedOf(addr string) int64

  • Claim(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}, amount int64, index int, auntsHex string)

  • ClaimCount() int

  • ClaimedAmount(addr string) int64

  • ClaimInstant(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}, amount int64, index int, auntsHex string)

  • ClaimWindowEnd() int64

  • CloseCampaign(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})

  • EcosystemFund() string

  • EndHeight() int64

  • Epoch() int

  • Forfeit(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}, amount int64, index int, auntsHex string)

  • FundOf(addr string) int64

  • GrantEpoch(addr string) int

  • HasClaimed(addr string) bool

  • HasGrant(addr string) bool

  • ImmediateBps() int

  • InstantBps() int

  • IsOpen() bool

  • IsSealed() bool

  • Leaf(addr string, amount int64) string

  • LeafCount() int

  • NextWithdrawAt(addr string) int64

  • Outstanding() int64

  • RealmAddress() string

  • Render(path string) string

  • Root() string

  • SealCampaign(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})

  • SetCampaign(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}, rootHex string, total int, end int64)

  • SettledBurned() int64

  • SettledToFund() int64

  • SettleUnclaimed(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})

  • TotalBurned() int64

  • TotalClaimed() int64

  • TotalGranted() int64

  • TotalToFund() int64

  • TransferOwnership(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}, newOwner string)

  • Vested(addr string) int64

  • VestingEnd() int64

  • VestingStart() int64

  • VestingStartOf(addr string) int64

  • Withdraw(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})

  • Withdrawable(addr string) int64

  • WithdrawCooldown() int64

  • WithdrawFor(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}, beneficiary string)

  • Withdrawn(addr string) int64

Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.

Rendered

RenderedRawgnoweb ↗

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.