feat(news): v5 legion - sponsorship, contributed-sats weight pricing, join floor - #11
Open
biwasxyz wants to merge 10 commits into
Open
feat(news): v5 legion - sponsorship, contributed-sats weight pricing, join floor#11biwasxyz wants to merge 10 commits into
biwasxyz wants to merge 10 commits into
Conversation
v5 = v4 with exactly two changes, for the sponsorship model: - news-treasury-v5.clar: adds a public sponsor-in(amount, memo) that funds the pool WITHOUT minting voting weight. A sponsor (or an agent on their behalf) pays for an ad, the money goes to the pool, and "sponsored by xyz" is rendered off-chain by the news site, so no governance power is gained. contribute-in (weight-minting) is unchanged; only sponsor money is un-welded from governance. - news-gov-v5.clar: DRAW_BPS 1 -> 5, so the draw is 0.05% (5x v4). Agents earn more per approved piece, sized against the larger pool sponsors bring in. news-gov-v5-testnet.clar regenerated by scripts/gen-testnet-gov.mjs (stacks blocks, DRAW_BPS 5). Tests: tests/news-v5.test.ts (51 cases incl. sponsor-in); full suite 166 green; clarinet check clean (8 contracts). Sponsorship needs no other on-chain machinery: the "sponsored by" badge and the deposit-to-story link live on the frontend; the txid is the sponsor's proof.
sponsor-in previously took one free-form memo, which forced every reader to agree on a separator convention to get a display name back out of it. The identity is now three typed arguments: a required name (the only attribution field), an optional link, and a free-form memo. The event prints them as distinct fields, so an indexer reads the name directly and parses nothing. Adds a MIN_SPONSOR floor of 100,000 sats (ERR_BELOW_MIN u450), readable via get-min-sponsor so callers need not hardcode it, and rejects an unnamed sponsor (ERR_EMPTY_NAME u451). There is deliberately no ceiling: a large deposit is a good customer, not an error. The explicit zero check is gone from sponsor-in only, since the floor subsumes it. The name is an UNVERIFIED claim. Any display must treat the sender principal as the authoritative identity. Nothing about sponsorship duration or ordering lives on-chain; those are off-chain rules anyone can recompute from these events. Also annotates propose-story with #[allow(unchecked_data)] in all five gov contracts, clearing five pre-existing clarinet warnings. Comment only, no behavior change: description is stored unvalidated alongside a length-checked title and link, and the checker flagged the odd one out. 170 tests green, clarinet check clean.
An audit of v5 found that sponsor-in broke an invariant v4's minting math
depended on. In v4 the pool moved only via contribute-in (which minted
weight in the same tx) or execute-payout (which shrank it), so the mint
rate could never move against a contributor and a non-empty pool implied
non-zero weight. sponsor-in is a weightless inflow and broke both halves.
Two confirmed consequences, reproduced in simnet:
- A sponsorship landing before the FIRST contributor left TotalWeight at
zero over a funded pool, so contribute's bootstrap branch minted a flat
`amount`: 10,000 sats bought 100% of a 100,010,000 pool. Every piece
that holder proposed then failed no-quorum (eligibleSnapshot u0), and
quote-weight(100_000_000) returned 9,999, under MIN_WEIGHT, so no
newcomer could clear the floor without paying pool-scale sats.
- Every later sponsorship raised the price of joining: 270M sponsored
against a 30M pool cut quote-weight(10_000) from 10,000 to 1,000. The
more sponsorship succeeded, the more closed the legion became, which
inverts the stated point of the weightless path.
The treasury now tracks WeightedBalance, the contributed share of the
pool, incremented by contribute-in only and shrunk pro-rata on payout so
it stays a true fraction of Balance. Gov prices new weight against that
via get-weighted-balance, in both contribute and quote-weight. The draw
still comes off the WHOLE pool, so sponsor sats enlarge every payout;
they simply buy no governance and move nobody's price.
Deliberate consequence: as sponsorship grows, governance is cheap relative
to the money it governs, since only contributors hold weight. That is the
un-welding working as intended, not a regression.
Also gates sponsor-in on gov being wired. It was the only entry point not
already gov-gated, and every outflow is, so before wiring it accepted
money into a treasury with no path out at all.
Tests: 6 new regressions covering sponsor-in COMPOSED with contribute,
which is the gap that let all of this through, plus a check pinning the
four sBTC principals together so a partial mainnet swap cannot ship a
contract that takes deposits in one token and pays out in another.
176 green, clarinet check clean.
…-division guarantee Round-2 audit follow-ups, all small. sponsor-in returned ERR_UNAUTHORIZED when gov was unwired, which reads as "you are not allowed" when it means "not ready yet". A sponsor UI cannot tell those apart. Now ERR_NOT_WIRED (u452). The M3 test pinned the four sBTC principals to each other but not to a value, so a mainnet swap that changed all four consistently to a typo'd principal still passed. It now asserts the expected principal. WeightedBalance reaching u0 while TotalWeight is > u0 would reopen gov's bootstrap branch and with it the original capture. Floor division prevents it, since (/ (* weighted amount) bal) equals weighted only for a payout taking the entire pool, which DRAW_BPS u5 cannot produce. That guarantee was implicit; it is now written down, because a larger DRAW_BPS or any admin drain would silently break it. 176 green, clarinet check clean.
contribute had no sats floor at all. It rejected only a zero amount and a contribution too small to mint any weight, so the real cost of joining was whatever the pool happened to make it. That cost only ever falls. Weight is priced as a share of the CONTRIBUTED pool, and a payout shrinks WeightedBalance while TotalWeight holds, so the sats needed to reach MIN_WEIGHT drop every time a story is paid. Sponsor deposits and other contributions are rate-neutral, so nothing pushes it back up. A mature legion could eventually be joined for dust, and full voting rights were already ten times cheaper than a sponsor badge. MIN_CONTRIBUTION u10000 is checked against the amount sent, not the weight it mints, so the entry price is a fixed number of sats whatever the pool is worth. It subsumes the old zero-amount check, which is removed along with its now-unused error constant; the dust check stays as a backstop. Set equal to MIN_WEIGHT deliberately. WeightedBalance <= TotalWeight always holds, so minted is never less than amount and a contribution at the floor always mints at least MIN_WEIGHT. Whoever can join can propose, vote and veto immediately: there is no tier of holders who paid in but cannot act. Also exposes minContribution in get-params so a UI reads it from chain. 179 green, clarinet check clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
v5 is the sponsorship model: money can enter the pool without buying governance, so a sponsor funds journalism and gets attribution instead of a vote.
It started as two changes to v4. An audit then found that the weight-less inflow broke an invariant v4's minting math silently depended on, and most of this PR is the fix for that.
1. Sponsorship
news-treasury-v5.claradds a publicsponsor-in(amount, name, link, memo)that adds sBTC to the pool and mints no voting weight.The sponsor's identity is structured, not packed into one string, so no indexer has to agree on a separator convention to get a display name back out:
name(string-ascii 40), non-emptylink(optional (string-ascii 96))memo(string-ascii 128)MIN_SPONSOR,ERR_BELOW_MINu450), readable viaget-min-sponsor. No ceiling: a large deposit is a good customer, not an error.ERR_NOT_WIREDu452). Every outflow is gov-gated, so before wiring there is no path out at all, and an unwired treasury would otherwise accept money it could never spend.nameis an unverified claim. Any display must treat the sender principal as the authoritative identity.2. A fixed floor to join
contributehad no sats floor at all, and the real cost of joining only ever fell: weight is priced as a share of the contributed pool, and payouts shrink that pool while total weight holds. A mature legion could eventually be joined for dust.MIN_CONTRIBUTIONu10000 (ERR_BELOW_MIN_CONTRIBUTIONu437) is checked against sats sent, so entry costs a fixed amount whatever the pool is worth. Set equal toMIN_WEIGHTdeliberately: becauseWeightedBalance <= TotalWeightalways holds, a contribution at the floor always mints at leastMIN_WEIGHT, so whoever can join can immediately propose, vote and veto.3. The audit fix: price weight against contributed sats
In v4 the pool moved only via
contribute-in(which minted weight in the same tx) orexecute-payout(which shrank it), so the mint rate could never move against a contributor and a non-empty pool implied non-zero weight. A weight-less inflow broke both halves. Two consequences, both reproduced in simnet:TotalWeightat zero over a funded pool, so the bootstrap branch minted a flatamount: 10,000 sats bought 100% of a 100,010,000 pool. Every piece that holder proposed then failed no-quorum, andquote-weight(100_000_000)returned 9,999, underMIN_WEIGHT, so no newcomer could clear the floor without pool-scale sats.quote-weight(10_000)from 10,000 to 1,000. The more sponsorship succeeded, the more closed the legion became, inverting the point of the weight-less path.The treasury now tracks
WeightedBalance, the contributed share of the pool: raised bycontribute-inonly, shrunk pro-rata on payout so it stays a true fraction ofBalance. Gov prices weight against it viaget-weighted-balance, in bothcontributeandquote-weight. The draw still comes off the whole pool, so sponsor sats enlarge every payout; they simply buy no governance and move nobody's price.Deliberate consequence, documented rather than hidden: as sponsorship grows, governance is cheap relative to the money it governs. That is the un-welding working as intended.
ECONOMICS.mdquantifies exactly how cheap.4. Draw 0.01% -> 0.05%
DRAW_BPS1 -> 5, so each approved piece pays 5x, sized against the larger pool sponsors bring in. Each draw is 0.05% of what's left, so the pool asymptotes rather than empties.Analysis
ECONOMICS.mdandECONOMICS-PLAIN.mddocument the economics and attack surface. Every threshold was verified in simnet at the boundary, one sat under the prediction and one sat at it, against thePROD-BURNmainnet build:17A/3 + p33A/17 + pmax(3A/17, 20k) + pFindings worth reviewer attention:
PROPOSE_INTERVAL), verified. Lengthening the lifecycle windows does not slow it, since the propose gate reads none of them; it only changes how many proposer principals are needed.VETO_WINDOW.VETO_QUORUM15% -> 5% raises attack cost 3.35x and is the strongest available lever. Not applied here; it is a product call.Contracts
news-treasury-v5.clar- v4 treasury +sponsor-in,WeightedBalance,MIN_SPONSORnews-gov-v5.clar- v4 gov +DRAW_BPS u5,MIN_CONTRIBUTION, priced againstget-weighted-balancenews-gov-v5-testnet.clar- generated byscripts/gen-testnet-gov.mjs, never hand-editeddeployments/news-v5.testnet-plan.yaml- fresh treasury, same deployer, treasury firstAlso annotates
propose-storywith#[allow(unchecked_data)]in all five gov contracts, clearing five pre-existing clarinet warnings. Comment-only, no behavior change. Notenews-gov.clarandnews-gov-v4-testnet.clarare deployed, so their source now differs from chain by that one line.Tests
tests/news-v5.test.ts- 64 cases. Full suite 179 green,clarinet checkclean with zero warnings.New coverage targets the gap that let the audit findings through:
sponsor-incomposed withcontribute, which nothing previously exercised. Plus a check pinning the four sBTC principals to each other and to an expected value, so a partial or typo'd mainnet swap cannot ship a contract that takes deposits in one token and pays out in another.Not done / open
.news-treasury-v5at 8 sites, so this repeats for any v6. On mainnet that pool would be real money.name. Escaping and link allow-listing are the app's job.sponsor-inevent: its chainhook filters on the gov contract, and sponsorships print on the treasury.SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token.