Skip to content

Commit 15a146b

Browse files
committed
feat(dividend): streaming dividends — a dividend every second
Add examples/dividend_stream/: a two-contract corporate dividend rail where the dividend accrues continuously at one-second granularity (tx.offchainTime), fixed in USD cents per unit-year, paid in sats at an oracle-attested BTC/USD price. Motivated by a hypothetical pilot for a listed Bitcoin treasury company: batch dividend rails cap ambition at monthly payouts and need record dates, transfer agents and distribution runs; continuous accrual removes the batch entirely (see dividend_stream.md). - DividendTreasury: singleton BTC reserve. Permissionless topUp (reserve only grows — public recurring-cash-flow time series), permissionless service of one claim per co-spend (recomputes accrual, pays the holder, and requires the position re-created with lastClaim = now so the same second is never paid twice), issuer-gated recall, CSV unilateral exit. - StreamingShare: per-holder position (units, lastClaim). claim co-spends the treasury (cross-input scriptPubKey binding via this.activeInputIndex, bonds-style); transfer is a pure key swap that carries accrual with the position (no ex-dividend cliff, no record date); split keeps lastClaim so it is exactly accrual-fair; CSV unilateral exit. - Accrual: units × annualDividendCents × elapsed / 31536000, int64-safe for realistic sizes; accruedCents > 0 guards basis-advance griefing; 330-sat dust floor on claims. - Tests: 13 behavioral tests (oracle message reconstruction, cross-input inspection, permissionless topUp/service, issuer-gated recall, key-swap purity, basis-advance recreation placeholders, CSV exits, synthesized cooperative leaves, shared constructor schema) + 2 roundtrip entries. Full suite green, cargo fmt clean. - Playground: Streaming Dividends project registered in main.js. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CmsjJ5rEDp6DE42kvauYD2
1 parent e81fad6 commit 15a146b

7 files changed

Lines changed: 806 additions & 0 deletions

File tree

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
# Streaming Dividends — a dividend every second
2+
3+
A Bitcoin-native dividend rail for corporate issuers: the dividend accrues
4+
continuously at one-second granularity, is fixed in USD cents per unit per
5+
year, and pays out in sats at an oracle-attested BTC/USD price whenever the
6+
holder chooses to settle. Two contracts implement the program:
7+
8+
| File | Contract | Role |
9+
|---|---|---|
10+
| `dividend_treasury.ark` | `DividendTreasury` | singleton BTC reserve; services claims, accepts top-ups, issuer recall |
11+
| `streaming_share.ark` | `StreamingShare` | per-holder position carrying `(units, lastClaim)`; claim / transfer / split |
12+
13+
## The pilot: a listed Bitcoin treasury company (hypothetical)
14+
15+
Picture a listed Bitcoin treasury company preparing perpetual preferred
16+
shares funded by its income-generation desk (covered calls, put premium —
17+
see `options/` and `hashprice/`). Two frictions recur in that plan wherever
18+
it is attempted:
19+
20+
- **The plumbing can't keep up with the ambition.** Paying dividends
21+
*monthly* is already exotic in most listed markets, where payouts are
22+
annual or semiannual; the issuer ends up building bespoke systems for
23+
shareholder verification, record-date management and recurring
24+
distribution runs.
25+
- **The listing gate is cash-flow credibility.** Exchange review asks for
26+
evidence that recurring dividends are backed by sustainable recurring cash
27+
flows — hard to show with a short operating track record and batch
28+
quarterly disclosure.
29+
30+
Both frictions are artifacts of *batch* settlement. This program removes the
31+
batch. On Arkade the dividend is not an event but a rate: every unit accrues
32+
`annualDividendCents / 31,536,000` per second, continuously, and any holder
33+
settles whenever they like. A monthly dividend makes 12 payments a year; this
34+
makes the concept of "a payment" disappear — 31.5 million accrual ticks a
35+
year, of which claims are just checkpoints.
36+
37+
The pilot shape that follows from this:
38+
39+
1. **Phase 0 — internal demo.** One treasury, a handful of positions on
40+
testnet. The income desk tops up the reserve on its real revenue cadence;
41+
a dashboard shows per-second accrual against reserve coverage.
42+
2. **Phase 1 — closed pilot.** A small tranche of *dividend entitlement
43+
units* (not the listed security itself) is allotted to qualified
44+
investors; each allotment is deployed as a `StreamingShare`. The listed
45+
preferred, when it lists, stays on the traditional register — the pilot
46+
instrument is the payment rail, mirroring registry holdings.
47+
3. **Phase 2 — the rail becomes the register.** Continuous on-chain top-ups
48+
from the income desk are themselves the auditable evidence of recurring
49+
cash flow that batch reporting struggles to demonstrate: reserve coverage
50+
is a public time series, not a quarterly disclosure.
51+
52+
Nothing here is legal advice; a real pilot needs securities counsel in the
53+
issuer's jurisdiction. The contracts are jurisdiction-agnostic.
54+
55+
## Why streaming kills the record date
56+
57+
A record date exists because batch rails must snapshot ownership to know whom
58+
to pay. Continuous accrual makes the snapshot meaningless:
59+
60+
- `transfer` is a key swap that carries `(units, lastClaim)` unchanged —
61+
accrued-but-unclaimed dividends travel with the position and are priced
62+
into the sale. Sell at 14:03:07 and you have earned exactly through
63+
14:03:07. There is no ex-dividend cliff, so no dividend-capture trade.
64+
- `split` gives both children the parent's `lastClaim`; accrual is linear in
65+
units, so splits are exactly accrual-fair with no division.
66+
- `claim` settles accrual and advances `lastClaim` to now, atomically with
67+
the payout. Claims are independent per holder — no distribution run.
68+
69+
## Mechanics
70+
71+
**Accrual.** Time is `tx.offchainTime` (TEE-attested wallclock, unix
72+
seconds), giving one-second granularity:
73+
74+
```
75+
elapsed = now − lastClaim
76+
accruedCents = units × annualDividendCents × elapsed / 31536000
77+
payoutSats = accruedCents × 1e8 / oraclePrice // price in cents/BTC
78+
```
79+
80+
The product `units × annualDividendCents × elapsed` must stay inside int64:
81+
1e6 units at $6.00/yr unclaimed for ten years is 1.9e17 — comfortably safe.
82+
Truncation loses under one cent per claim, and `accruedCents > 0` guards a
83+
zero-payout claim from advancing `lastClaim`.
84+
85+
**Claim transaction.** A claim co-spends treasury and position; each covenant
86+
independently recomputes the accrual and protects its own side:
87+
88+
```
89+
input[srvIdx]: DividendTreasury (service — permissionless)
90+
input[posIdx]: StreamingShare (claim — holder-signed)
91+
output[0]: treasury re-created, reserveSats − payoutSats
92+
output[1]: position re-created, lastClaim = now
93+
output[2]: payoutSats → holder
94+
```
95+
96+
The treasury verifies the co-spent input is a genuine `StreamingShare` with
97+
the witness-declared `(holder, units, lastClaim)` (scriptPubKey equality
98+
binds the declaration), and only releases reserve against a position whose
99+
accrual basis simultaneously advances — the same second of dividend time can
100+
never be paid twice. The position verifies the holder authorized, the
101+
treasury is genuinely present, and the holder is paid. `service` itself is
102+
permissionless, so wallets or watchtowers can co-sponsor claims.
103+
104+
**Funding.** `topUp` is permissionless and monotone — the reserve only
105+
grows. The income desk streams revenue in on whatever cadence it earns it,
106+
which is exactly the "recurring cash flow" evidence exchanges ask for, as a
107+
public time series.
108+
109+
**Oracle.** Fuji-style signed feed, as in `stability/` and `hashprice/`:
110+
`msg = sha256(ticker || price || time)` verified with `checkSigFromStack`,
111+
freshness bounded to 600 seconds of `tx.offchainTime`. A claim needs a live
112+
oracle; accrual itself never does — if the oracle stalls, dividends keep
113+
accruing and claims resume with the feed.
114+
115+
**Dust.** Claims below 331 sats are rejected (`payoutSats > 330`), matching
116+
the Taproot dust convention used across the examples. At $6.00/yr per unit
117+
and $100k BTC, one unit crosses dust in about two days; a holder of N units
118+
can claim N times as often. Accrual is per-second; claiming is whenever it's
119+
worth a UTXO.
120+
121+
## Trust model — what the pilot does and doesn't enforce
122+
123+
- **Reserve coverage is visible, not mandatory.** `recall` lets the issuer
124+
withdraw reserve at will; claims fail when the reserve is exhausted. Every
125+
recall and every top-up is on-chain the moment it happens, so coverage is
126+
watchable in real time — but solvency is a legal obligation of the issuer,
127+
not a covenant. Hardening path (deliberately out of pilot scope): a
128+
timelocked recall notice, and the arrears clock + penalty-rate mechanics
129+
prototyped in the `VariableDividendPreferred` example (PR #40).
130+
- **`totalUnits` is fixed at deployment.** The treasury sanity-checks
131+
positions against it, but issuance integrity (that allotted positions sum
132+
to `totalUnits`) is an off-chain deployment fact. An issuance-controlled
133+
asset gating position creation — the `hashprice/` identity-singleton
134+
pattern — is the natural upgrade.
135+
- **Clock and oracle.** `tx.offchainTime` is TEE wallclock: not
136+
consensus-enforced, and guarded against regression but not against pause.
137+
Oracle liveness gates claims, never accrual.
138+
- **Exit.** Both contracts carry CSV unilateral-exit leaves. The position
139+
UTXO's sat value is dust; what exits is the *instrument*, whose entitlement
140+
is then enforced against the issuer per program terms — same model as any
141+
registered security, with a better audit trail.
142+
143+
## Relation to the other examples
144+
145+
- `stability/` — the oracle witness, `tx.offchainTime` accrual and
146+
per-second rate idioms; this program is those idioms turned into a
147+
corporate-action rail.
148+
- `bonds/` — the two-contract co-spend pattern (`repayment_pool`
149+
`bond_mint`) that `service``claim` follows.
150+
- `hashprice/`, `options/` — the income side: covered calls and hashprice
151+
vaults are ways a treasury desk *earns* the BTC that `topUp` streams into
152+
this reserve.
153+
- `VariableDividendPreferred` (PR #40) — the single-UTXO, per-holder
154+
preferred with arrears teeth; this program is its pooled, streaming,
155+
transfer-friendly successor.
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
// DividendTreasury Contract
2+
//
3+
// The issuer-funded reserve of a streaming-dividend program: a corporate
4+
// treasury (e.g. a Bitcoin treasury company's income-generation desk) escrows
5+
// BTC that services dividend claims from StreamingShare positions. The
6+
// dividend is fixed in USD cents per unit per year, accrues continuously at
7+
// one-second granularity (tx.offchainTime), and is paid in sats at an
8+
// oracle-attested BTC/USD price at claim time.
9+
//
10+
// ── Why streaming ───────────────────────────────────────────────────────────
11+
// Batch dividend rails need record dates, transfer agents and distribution
12+
// runs; paying monthly is already exotic in most listed markets. Here the
13+
// entitlement accrues per second and any holder can settle their accrued
14+
// balance whenever it exceeds dust — the record date disappears because
15+
// accrual follows the position continuously (see dividend_stream.md).
16+
//
17+
// ── Two contracts, one program ──────────────────────────────────────────────
18+
// DividendTreasury (this file) — singleton reserve. Pays claims, accepts
19+
// top-ups from the income desk, lets the issuer recall unallocated
20+
// reserve (telegraphed on-chain; see trust model in dividend_stream.md).
21+
// StreamingShare — per-holder position carrying (units, lastClaim). A claim
22+
// co-spends both: the treasury verifies the position is genuine and that
23+
// its accrual basis advances; the position verifies the holder is paid.
24+
//
25+
// ── Claim transaction layout (service ⊗ claim) ──────────────────────────────
26+
// input[srvIdx]: this treasury (function: service)
27+
// input[posIdx]: a StreamingShare (function: claim)
28+
// output[0]: treasury re-created, reserveSats − payoutSats
29+
// output[1]: position re-created, lastClaim = tx.offchainTime
30+
// output[2]: payoutSats → holder (SingleSig)
31+
//
32+
// ── Accrual math (shared with StreamingShare) ───────────────────────────────
33+
// elapsed = tx.offchainTime − posLastClaim [seconds]
34+
// accruedCents = units × annualDividendCents × elapsed / 31536000
35+
// payoutSats = accruedCents × 1e8 / oraclePrice [price in cents/BTC]
36+
// int64 bound: units × annualDividendCents × elapsed < 9.2e18 — e.g. 1e6
37+
// units at $6.00/yr (600 cents) unclaimed for 10 years is 1.9e17, safe.
38+
// Truncation loses < 1 cent per claim; the accruedCents > 0 guard prevents
39+
// a zero-payout claim from advancing lastClaim (no accrual griefing).
40+
//
41+
// ── Oracle model (Fuji-style signed price feed) ─────────────────────────────
42+
// msg = sha256(ticker || price || timestamp), price/timestamp 8-byte LE.
43+
// Freshness: tx.offchainTime − oracleTime within [0, 600] seconds.
44+
45+
import "single_sig.ark";
46+
import "streaming_share.ark";
47+
48+
contract DividendTreasury(
49+
pubkey issuerPk, // issuer treasury key: recall + unilateral exit
50+
pubkey oraclePk, // BTC/USD signed price feed
51+
bytes32 ticker, // feed id, e.g. sha256("BTC/USD")
52+
int annualDividendCents, // fixed dividend per unit per year, USD cents
53+
int totalUnits, // units outstanding across all positions
54+
int reserveSats, // sats escrowed for dividends; mutates on every spend
55+
int exit // exit timelock in blocks (shared with positions)
56+
) {
57+
58+
// -------------------------------------------------------------------------
59+
// TOP UP — permissionless. The income desk (or anyone) streams revenue into
60+
// the reserve. No sats can leave: the reserve only grows.
61+
// output[0]: treasury re-created with reserveSats + amount
62+
// -------------------------------------------------------------------------
63+
function topUp(int amount) {
64+
require(amount > 0, "zero amount");
65+
66+
int newReserve = reserveSats + amount;
67+
68+
require(
69+
tx.outputs[0].scriptPubKey == new DividendTreasury(
70+
issuerPk, oraclePk, ticker, annualDividendCents, totalUnits,
71+
newReserve, exit
72+
),
73+
"invalid treasury recreation"
74+
);
75+
require(tx.outputs[0].value >= newReserve, "reserve not deposited");
76+
}
77+
78+
// -------------------------------------------------------------------------
79+
// SERVICE — permissionless servicing of one position's claim (the position
80+
// input carries the holder's authorization). Verifies the co-spent input is
81+
// a genuine StreamingShare with the witness-declared state, recomputes the
82+
// accrual independently, pays the holder, and requires the position to be
83+
// re-created with its accrual basis advanced to now — the reserve can only
84+
// leave against dividend time that is simultaneously consumed.
85+
// -------------------------------------------------------------------------
86+
function service(
87+
int posIdx,
88+
pubkey posHolderPk,
89+
int posUnits,
90+
int posLastClaim,
91+
int oraclePrice,
92+
int oracleTime,
93+
signature oracleSig
94+
) {
95+
require(this.activeInputIndex != posIdx, "self-service disallowed");
96+
require(posUnits > 0, "empty position");
97+
require(posUnits <= totalUnits, "position exceeds issue");
98+
require(oraclePrice > 0, "invalid oracle price");
99+
100+
require(
101+
tx.inputs[posIdx].scriptPubKey == new StreamingShare(
102+
posHolderPk, issuerPk, oraclePk, ticker,
103+
annualDividendCents, posUnits, posLastClaim, exit
104+
),
105+
"input not matching position"
106+
);
107+
108+
int oracleAge = tx.offchainTime - oracleTime;
109+
require(oracleAge >= 0, "future-dated oracle");
110+
require(oracleAge <= 600, "stale oracle");
111+
let oracleMsg = sha256(ticker + oraclePrice + oracleTime);
112+
require(checkSigFromStack(oracleSig, oraclePk, oracleMsg), "invalid oracle signature");
113+
114+
int elapsed = tx.offchainTime - posLastClaim;
115+
require(elapsed >= 0, "clock regression");
116+
int annualCents = posUnits * annualDividendCents;
117+
int accruedCents = annualCents * elapsed / 31536000;
118+
require(accruedCents > 0, "nothing accrued");
119+
int payoutSats = accruedCents * 100000000 / oraclePrice;
120+
require(payoutSats > 330, "dust claim; accrue longer");
121+
require(payoutSats <= reserveSats, "reserve exhausted");
122+
123+
int newReserve = reserveSats - payoutSats;
124+
125+
require(
126+
tx.outputs[0].scriptPubKey == new DividendTreasury(
127+
issuerPk, oraclePk, ticker, annualDividendCents, totalUnits,
128+
newReserve, exit
129+
),
130+
"invalid treasury recreation"
131+
);
132+
require(tx.outputs[0].value >= newReserve, "reserve not preserved");
133+
134+
require(
135+
tx.outputs[1].scriptPubKey == new StreamingShare(
136+
posHolderPk, issuerPk, oraclePk, ticker,
137+
annualDividendCents, posUnits, tx.offchainTime, exit
138+
),
139+
"position basis not advanced"
140+
);
141+
require(tx.outputs[1].value >= 330, "position output dust");
142+
143+
require(tx.outputs[2].scriptPubKey == new SingleSig(posHolderPk, exit), "output 2 not holder");
144+
require(tx.outputs[2].value >= payoutSats, "holder underpaid");
145+
}
146+
147+
// -------------------------------------------------------------------------
148+
// RECALL — issuer-gated reserve withdrawal (program wind-down or resizing).
149+
// Recalls are visible on-chain the moment they happen, so holders can see
150+
// reserve coverage shrink and act; the pilot trust model and its hardening
151+
// path (timelocked recall notice, arrears penalties) are documented in
152+
// dividend_stream.md.
153+
// output[0]: treasury re-created with reserveSats − amount
154+
// output[1]: amount → issuer
155+
// -------------------------------------------------------------------------
156+
function recall(signature issuerSig, int amount) {
157+
require(checkSig(issuerSig, issuerPk), "invalid issuer sig");
158+
require(amount > 0, "zero amount");
159+
require(amount <= reserveSats, "exceeds reserve");
160+
161+
int newReserve = reserveSats - amount;
162+
163+
require(
164+
tx.outputs[0].scriptPubKey == new DividendTreasury(
165+
issuerPk, oraclePk, ticker, annualDividendCents, totalUnits,
166+
newReserve, exit
167+
),
168+
"invalid treasury recreation"
169+
);
170+
require(tx.outputs[0].value >= newReserve, "reserve not preserved");
171+
172+
require(tx.outputs[1].scriptPubKey == new SingleSig(issuerPk, exit), "output 1 not issuer");
173+
require(tx.outputs[1].value >= amount, "issuer underpaid");
174+
}
175+
176+
// -------------------------------------------------------------------------
177+
// UNILATERAL EXIT (CSV) — issuer recovers the reserve if the Arkade
178+
// operator goes offline. The issuer is the residual claimant of the
179+
// reserve; holder positions carry their own exit leaf.
180+
// -------------------------------------------------------------------------
181+
function unilateral(signature issuerSig) tapscript {
182+
require(older(exit));
183+
require(checkSig(issuerSig, issuerPk));
184+
}
185+
}

0 commit comments

Comments
 (0)