Skip to content

feat: add snapshot-based dividend distribution contract (#1073) - #1159

Merged
Chucks1093 merged 2 commits into
accesslayerorg:mainfrom
Ajibose:feat/dividend-distribution-contract
Oct 26, 2026
Merged

Chucks1093 merged 2 commits into
accesslayerorg:mainfrom
Ajibose:feat/dividend-distribution-contract

Conversation

@Ajibose

@Ajibose Ajibose commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

Closes #1073

Summary

Adds a snapshot-based dividend distribution contract that lets creators distribute revenue to their key holders pro-rata to holdings. Holder balances are frozen in an on-chain snapshot at the distribution block, so purchases made after the snapshot can never increase anyone's entitlement — closing the flash-purchase gaming vector that a balance-at-claim model would have.

What was implemented

  • snapshot_holders(creator, key_id) — creator-only entry point that reads the key's holder registry and every holder's current balance from the creator-keys contract at call time and freezes them in a DividendSnapshot record (snapshot id, key, creator, snapshot ledger, total supply, holder count, funded/claimed totals). Returns the new snapshot id and emits DividendSnapshotCreated.
  • fund_dividend(creator, key_id, amount) — creator-only funding of the latest snapshot's distribution pool. Funding accumulates across calls and can happen at any time after the snapshot; entitlements are fixed by the snapshot balances, so funding later never re-prorates holders. Emits DividendFunded.
  • claim_dividend(holder, snapshot_id) — pays funded_amount * snapshot_balance / total_supply (floor division, dust stays in the pool), marks the holder claimed, records the settled amount, and emits DividendClaimed with the snapshot id and amount. A second claim of the same snapshot reverts with AlreadyClaimed.
  • get_unclaimed_dividend(wallet, snapshot_id) — read-only view returning the claimable amount (0 for unknown snapshots, zero balance, unfunded pools, or after claiming).
  • Supporting views: get_snapshot, get_snapshot_holders, get_snapshot_holder_balance, has_claimed, get_paid_amount, get_latest_snapshot_id, get_total_funded, get_total_claimed, get_admin, get_creator_keys_contract.
  • One-time initialize(admin, creator_keys_contract) binds the contract to the deployed creator-keys instance it snapshots.
  • Errors in a dedicated DividendError enum (NotInitialized, AlreadyInitialized, Unauthorized, NotPositiveAmount, SnapshotNotFound, AlreadyClaimed, NothingToClaim, NoKeyHolders, HolderLimitExceeded, Overflow) following the workspace pattern of per-feature error enums.
  • A MAX_SNAPSHOT_HOLDERS (100) bound mirroring creator-keys::MAX_SNAPSHOT_HOLDERS so the snapshot call stays inside the Soroban instruction budget.
  • Persistent storage extended to the full protocol TTL window (creator_keys::CREATOR_TTL_LEDGERS) on every write, so snapshots stay claimable for the same horizon as creator state.

Files

New files

  • dividend-contract/Cargo.toml — new workspace member crate (soroban-sdk + path dependency on creator-keys)
  • dividend-contract/src/lib.rs — contract implementation (618 lines)
  • dividend-contract/src/test.rs — test suite (648 lines)

Modified files

  • Cargo.toml — added dividend-contract to the workspace members
  • Cargo.lock — lockfile entries for the new crate

Test files

  • dividend-contract/src/test.rs — 23 tests (all new; no existing tests were modified)

Implementation details

  • The contract follows the repository's standalone-contract pattern (lp-reward-contract, creator-keys-factory): #![no_std], #[contract]/#[contractimpl], its own #[contracttype] DataKey enum kept out of creator-keys::DataKey, short symbol_short! event topics with typed event data structs, and #[cfg(test)] mod test;.
  • Holder discovery is on-chain and trust-minimized: unlike caller-supplied holder lists (take_snapshot, Implement dividend pool contract with pro-rata distribution logic #971), snapshot_holders takes only key_id and reads get_holder_registry / get_balance from the creator-keys contract via a cross-contract call, so recorded balances are the canonical ledger state at the snapshot ledger.
  • Snapshot ids are global and sequential (starting at 1); LatestSnapshot(key_id) routes fund_dividend to the key's most recent round, so multiple distribution rounds per key stay independent.
  • Funding and claims are internal-ledger accounting, consistent with the protocol's existing dividend flows (distribute_dividend_claimable Add a dividend claim function storing unclaimed dividends and allowing holders to withdraw at any time #857) — no token moves inside the contract, and every settled amount is recoverable from the PaidAmount ledger and the DividendClaimed event.
  • Claims are guarded with defense-in-depth: require_auth plus explicit snapshot.creator/snapshot.key_id identity checks on funding, and an explicit claimed-flag check before any payout.

Tests added

23 tests in dividend-contract/src/test.rs, covering every acceptance criterion:

  1. Snapshot records correct holder balances at time of call — test_snapshot_records_correct_holder_balances (per-holder balances, total supply, holder count, holders list), test_snapshot_records_ledger_at_call_time (snapshot ledger frozen even after later activity).
  2. Pro-rata claim computed correctly from snapshot balances — test_claim_pays_pro_rata_share_of_funded_pool (90/10 → 900/100 of 1,000), test_get_unclaimed_dividend_reports_claimable_amount, test_fund_dividend_accumulates_and_reports_totals.
  3. Post-snapshot purchases do not affect snapshot entitlements — test_post_snapshot_purchases_do_not_affect_entitlements (flash purchaser gets 0), test_selling_after_snapshot_keeps_entitlement (sellers keep their frozen claim), test_multiple_snapshots_are_independent.
  4. claim_dividend reverts on double-claim with AlreadyClaimed — test_double_claim_reverts_with_already_claimed, plus test_claim_unknown_snapshot_reverts, test_claim_reverts_for_non_holder, test_claim_reverts_on_unfunded_snapshot.
  5. Both events emitted with correct snapshot and amount data — test_dividend_snapshot_created_event_emitted_with_correct_data, test_dividend_claimed_event_emitted_with_correct_amount.
  6. Authorization and guard rails — test_snapshot_requires_creator_auth, test_snapshot_requires_initialization, test_initialize_rejects_second_call, test_snapshot_on_key_without_holders_reverts, test_snapshot_exceeding_holder_limit_reverts, test_fund_dividend_requires_snapshot, test_fund_dividend_rejects_non_creator, test_fund_dividend_rejects_non_positive_amount, test_views_return_defaults_for_unknown_snapshot.

How to test

# Run the new contract's suite (23 tests)
cargo test -p dividend-contract

# Run the full workspace suite (what CI runs)
cargo test --workspace

# CI lint gates
cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings

Verified locally: cargo test -p dividend-contract → 23 passed; full workspace test run → 0 failures across all crates (488 creator-keys lib tests + every integration target); cargo fmt --all -- --check and cargo clippy --workspace --all-targets -- -D warnings both clean.

…rg#1073)

Implement a standalone dividend-contract crate that lets creators share
revenue with their key holders pro-rata to holdings, using a snapshot-based
distribution mechanism to prevent gaming through flash purchases:

- snapshot_holders(key_id): creator-only; freezes every holder balance read
  from the creator-keys holder registry and ledger at the time of the call
- fund_dividend(key_id, amount): creator funds the latest snapshot's
  distribution pool; funding accumulates across calls
- claim_dividend(snapshot_id): pays funded_amount * holder_balance /
  total_supply and reverts with AlreadyClaimed on double-claim
- get_unclaimed_dividend(wallet, snapshot_id): read-only claimable amount
- emit DividendSnapshotCreated and DividendClaimed events carrying the
  snapshot id and amount data, plus DividendFunded for top-ups
- register dividend-contract in the cargo workspace and add 23 tests
  covering all acceptance criteria from the issue
@drips-wave

drips-wave Bot commented Oct 19, 2026

Copy link
Copy Markdown

@Ajibose Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@Chucks1093

Copy link
Copy Markdown
Member

⚠️ Merge Conflict Detected

This PR currently has a merge conflict and cannot be merged until it is resolved.

Please rebase or merge the latest changes from the base branch into your branch and push again.

1 similar comment
@Chucks1093

Copy link
Copy Markdown
Member

⚠️ Merge Conflict Detected

This PR currently has a merge conflict and cannot be merged until it is resolved.

Please rebase or merge the latest changes from the base branch into your branch and push again.

@Chucks1093

Copy link
Copy Markdown
Member

✅ PR Ready for Review

Due to a recent malware incident affecting our repositories, all PRs are required to pass automated security checks before being reviewed for merging.

This PR has passed all automated checks:

  • ✅ Security scan — no malicious code detected
  • ✅ No merge conflicts
  • ✅ CI checks passed

Next step: Please share this PR link in our Telegram group so a maintainer can review and merge it: https://t.me/c/accesslayerorg/8514

@Chucks1093
Chucks1093 merged commit e794fda into accesslayerorg:main Oct 26, 2026
1 check passed
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.

Add dividend distribution contract for creator revenue sharing with key holders

2 participants