Skip to content

feat: VariableDividendPreferred — variable-rate perpetual preferred example#40

Draft
tiero wants to merge 1 commit into
masterfrom
claude/strc-dividend-arkade-5AB6a
Draft

feat: VariableDividendPreferred — variable-rate perpetual preferred example#40
tiero wants to merge 1 commit into
masterfrom
claude/strc-dividend-arkade-5AB6a

Conversation

@tiero

@tiero tiero commented Jun 6, 2026

Copy link
Copy Markdown
Member

Summary

Adds a new example contract, examples/variable_dividend_preferred.ark: a Bitcoin-native perpetual preferred share paying a variable cash dividend, callable by the issuer at par + accrued. It's a $100-par claim whose dividend rate is continuously re-pegged to keep the share trading near par — a generalized, on-chain model of a variable-rate perpetual preferred. Dividends are denominated in USD cents and paid in BTC (sats) at an oracle-attested BTC/USD price.

This is an example/reference contract (in the same family as stability/, bonds/, and options/), not a change to the compiler itself.

Dividend design

  • Continuous pro-rata accrual on par (faceValue × units) at dividendRateBps, using interleaved divides to stay inside int64.
  • CumulativeaccruedCents is only ever reset to 0 by an actual payment (claim/redeem); there is no partial-claim path, so the debt accounting stays honest.
  • Deterministic re-peg from a signed secondary-price oracle feed: below par forces the rate up, above par down, clamped to [rateMinBps, rateMaxBps]. The issuer chooses when to re-peg, never how much.
  • Arrears protection (teeth) via a self-revealing coverage trick: because the reserve is sats but the obligation is cents, coverage needs the BTC/USD oracle, and the covenant can only assert lower bounds on output value. So the spender declares the next arrearsSince — declaring "healthy" forces full reserve coverage, otherwise an honest, non-resettable arrears clock starts. Past gracePeriod the effective accrual rate jumps to penaltyRateBps and the holder may forceRedeem the reserve. pokeArrears is permissionless, so the issuer can't dodge the penalty by staying idle.

Functions

accrueAndRepeg, pokeArrears, claim, topUp, transfer, redeem, forceRedeem — each emitting both the cooperative and the timelock-gated unilateral-exit variant (14 tapleaves).

Tests

  • roundtrip_variable_dividend_preferred in compilation_roundtrip_test.rs — structural invariants + 14-tapleaf count.
  • New tests/variable_dividend_preferred_test.rs — behavioral coverage: oracle message reconstruction (sha256(ticker‖price‖time) + OP_CHECKSIGFROMSTACK), pure key-swaps touch no oracle, pokeArrears is permissionless, and every function exposes a CSV-gated exit variant.

cargo fmt --check clean; full cargo test green. playground/contracts.js regenerated locally (gitignored artifact, not committed).

Notes / open design choices

Deliberately kept simple — flat penalty (no per-window ratchet), uncapped forceRedeem seizure, and arrears enforcement living in pokeArrears rather than folded into the re-peg. Happy to tighten any of these. Marked draft for review of the dividend/arrears model before finalizing.

https://claude.ai/code/session_016X8b5rz5zPuxz8QGGCiugQ


Generated by Claude Code

A Bitcoin-native perpetual preferred share paying a variable cash dividend,
callable by the issuer at par + accrued. A $100-par claim whose dividend rate
is re-pegged to keep the share trading near par. Dividends are denominated in
USD cents and paid in BTC (sats) at an oracle-attested BTC/USD price.

Dividend design:
- Continuous pro-rata accrual on par at dividendRateBps.
- Cumulative: accruedCents only ever resets on a real payment (claim/redeem).
- Deterministic re-peg from a signed secondary-price oracle feed: below par
  forces the rate up, above par down, clamped to [min,max].
- Arrears teeth: self-revealing coverage (declaring "healthy" forces full
  reserve coverage; otherwise an honest, non-resettable arrears clock starts).
  Past grace the effective rate jumps to penaltyRateBps and the holder may
  forceRedeem. pokeArrears is permissionless so the issuer can't dodge it.

Tests: roundtrip structural invariants + behavioral coverage (oracle message
reconstruction, pure key-swaps, permissionless poke, timelocked exit variants).
@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0c3a77f5-bb2b-4091-b67a-21937567ca48

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/strc-dividend-arkade-5AB6a

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Playground Preview

A live preview of this PR's playground is available at:
https://arkade-os.github.io/compiler/pr-previews/pr-40/

Built from commit 997f12e6be1411ecb03e7c2cb98cb44895b643ed · Workflow run

@arkanaai arkanaai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arkana Code Review — VariableDividendPreferred

Thorough review. The contract is well-documented and the dividend/arrears model is clever. However, there are critical value-preservation gaps that would allow reserve drainage, rendering the arrears protection hollow.


🔴 CRITICAL — Permissionless pokeArrears allows reserve drain

examples/variable_dividend_preferred.ark:170pokeArrears is intentionally permissionless (no issuer/holder sig), but the only output-value check is tx.outputs[0].value >= 330 (line 170). Anyone who can construct a valid transaction (cooperative path with ASP, or after CSV expiry on exit path) can:

  1. Call pokeArrears with a valid oracle message
  2. Set tx.outputs[0].value = 330 (dust floor)
  3. Route the remaining reserve to an output they control

Existing permissionless functions in this repo (e.g., BondMint.liquidate) either enforce exact value on outputs or are paired with co-spent contracts that handle precise accounting. pokeArrears has neither.

Fix: At minimum, enforce tx.outputs[0].value >= tx.inputs[0].value (value preservation), or require the output value covers accrued + some margin. Alternatively, gate pokeArrears on a signature (holder or a watchtower key) while still keeping it independent of the issuer.


🔴 CRITICAL — accrueAndRepeg lets issuer drain reserve silently

examples/variable_dividend_preferred.ark:118-119 — Same pattern: tx.outputs[0].value >= 330. The issuer calls accrueAndRepeg, sets output[0] to 330 sats, and pockets the rest. The arrears clock doesn't even start because arrearsSince is carried unchanged — the holder has to notice and call pokeArrears, wait for the grace period, then forceRedeem… 330 sats.

This makes the entire arrears protection mechanism hollow. The issuer can drain first, face consequences never.

Fix: accrueAndRepeg should preserve value: tx.outputs[0].value >= tx.inputs[0].value (or at least the current UTXO value). The issuer already has topUp and can withdraw excess through a dedicated function with proper guardrails.


🟡 HIGH — Integer overflow on faceValue * units and newAccrued * 100000000

examples/variable_dividend_preferred.ark:102,103 (and repeated in every function) — int notional = faceValue * units can overflow int64 if both are large. Similarly, newAccrued * 100000000 (line 152) overflows when newAccrued > ~92 billion cents (~$920M).

Existing contracts in this repo document the overflow ceiling (bond_mint comments reference ~92 BTC). This contract should either:

  • Add require() guards on faceValue and units bounds, or
  • Document the safe operating range explicitly

🟡 HIGH — delta > 0 requirement blocks rapid repeg

examples/variable_dividend_preferred.ark:105require(delta > 0, "no accrual; wait longer") means you cannot repeg without non-zero accrual. Due to integer truncation in rateElapsed = effRate * elapsed / 31536000, small elapsed values produce rateElapsed = 0, so delta = 0.

For a 500 bps rate, minimum elapsed before any accrual: 31536000 / 500 = 63072 seconds ≈ 17.5 hours. During volatile markets, being unable to repeg for 17+ hours defeats the purpose of the re-peg mechanism.

Fix: Allow delta >= 0 (zero accrual is fine; the state change is the repeg itself). Or separate the repeg from the accrual into distinct functions.


🟡 MEDIUM — topUp allows value decrease

examples/variable_dividend_preferred.ark:216require(tx.outputs[0].value >= 330). The function is called "topUp" but there's no enforcement that the output value exceeds the input value. An issuer could "top up" while actually withdrawing funds.

Fix: Enforce tx.outputs[0].value > tx.inputs[0].value or at minimum tx.outputs[0].value >= currentValue if input introspection is available.


🟡 MEDIUM — claim clears arrears even when structurally underfunded

examples/variable_dividend_preferred.ark:199-200 — A successful claim sets both accruedCents = 0 and arrearsSince = 0. But paying a small accrued dividend doesn't mean the reserve adequately covers future obligations. An issuer can keep the reserve barely above the current accrued amount, claim clears the clock, and repeat — never building a meaningful reserve.

This is a design choice but weakens the arrears protection. Consider: arrears should only clear when reserve coverage is verified (as in pokeArrears healthy-declaration logic).


🟢 LOW — No constructor validation of faceValue > 0, units > 0

If a contract is instantiated with faceValue = 0 or units = 0, then notional = 0delta = 0 → every function fails on require(delta > 0). The contract becomes permanently stuck. Not a security issue but a footgun.


🟢 LOW — forceRedeem seizure may be worthless

examples/variable_dividend_preferred.ark:339require(tx.outputs[0].value >= 330). If the reserve has already been drained (via the value-preservation gaps above), forceRedeem seizes 330 sats. The "teeth" have no bite. This is downstream of the critical issues above — fixing value preservation fixes this.


Tests

The tests (variable_dividend_preferred_test.rs) are well-structured and verify the right structural invariants (oracle verification, permissionless pokeArrears, timelocked exits). However:

  • No tests verify value-preservation semantics (because the contract doesn't enforce them)
  • No overflow scenario coverage
  • No test for the repeg math with small elapsed intervals

Summary

The dividend model is sound and the arrears design is thoughtful, but value preservation is not enforced, which is a pattern violation vs. other contracts in this repo. The critical issues (reserve drainage via pokeArrears and accrueAndRepeg) must be fixed before merge — they make the entire arrears protection mechanism a dead letter.

Requesting changes on the two critical items. Happy to re-review once addressed.

Reviewed by Arkana

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants