Repository navigation
feat: add snapshot-based dividend distribution contract (#1073) - #1159
Conversation
…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
|
@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! 🚀 |
|
1 similar comment
|
…ibution-contract # Conflicts: # Cargo.toml
✅ PR Ready for ReviewDue 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:
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 |
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 thecreator-keyscontract at call time and freezes them in aDividendSnapshotrecord (snapshot id, key, creator, snapshot ledger, total supply, holder count, funded/claimed totals). Returns the new snapshot id and emitsDividendSnapshotCreated.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. EmitsDividendFunded.claim_dividend(holder, snapshot_id)— paysfunded_amount * snapshot_balance / total_supply(floor division, dust stays in the pool), marks the holder claimed, records the settled amount, and emitsDividendClaimedwith the snapshot id and amount. A second claim of the same snapshot reverts withAlreadyClaimed.get_unclaimed_dividend(wallet, snapshot_id)— read-only view returning the claimable amount (0 for unknown snapshots, zero balance, unfunded pools, or after claiming).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.initialize(admin, creator_keys_contract)binds the contract to the deployedcreator-keysinstance it snapshots.DividendErrorenum (NotInitialized,AlreadyInitialized,Unauthorized,NotPositiveAmount,SnapshotNotFound,AlreadyClaimed,NothingToClaim,NoKeyHolders,HolderLimitExceeded,Overflow) following the workspace pattern of per-feature error enums.MAX_SNAPSHOT_HOLDERS(100) bound mirroringcreator-keys::MAX_SNAPSHOT_HOLDERSso the snapshot call stays inside the Soroban instruction budget.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 oncreator-keys)dividend-contract/src/lib.rs— contract implementation (618 lines)dividend-contract/src/test.rs— test suite (648 lines)Modified files
Cargo.toml— addeddividend-contractto the workspace membersCargo.lock— lockfile entries for the new crateTest files
dividend-contract/src/test.rs— 23 tests (all new; no existing tests were modified)Implementation details
lp-reward-contract,creator-keys-factory):#![no_std],#[contract]/#[contractimpl], its own#[contracttype]DataKeyenum kept out ofcreator-keys::DataKey, shortsymbol_short!event topics with typed event data structs, and#[cfg(test)] mod test;.take_snapshot, Implement dividend pool contract with pro-rata distribution logic #971),snapshot_holderstakes onlykey_idand readsget_holder_registry/get_balancefrom thecreator-keyscontract via a cross-contract call, so recorded balances are the canonical ledger state at the snapshot ledger.LatestSnapshot(key_id)routesfund_dividendto the key's most recent round, so multiple distribution rounds per key stay independent.distribute_dividend_claimableAdd 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 thePaidAmountledger and theDividendClaimedevent.require_authplus explicitsnapshot.creator/snapshot.key_ididentity 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: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).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.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.claim_dividendreverts on double-claim withAlreadyClaimed—test_double_claim_reverts_with_already_claimed, plustest_claim_unknown_snapshot_reverts,test_claim_reverts_for_non_holder,test_claim_reverts_on_unfunded_snapshot.test_dividend_snapshot_created_event_emitted_with_correct_data,test_dividend_claimed_event_emitted_with_correct_amount.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
Verified locally:
cargo test -p dividend-contract→ 23 passed; full workspace test run → 0 failures across all crates (488creator-keyslib tests + every integration target);cargo fmt --all -- --checkandcargo clippy --workspace --all-targets -- -D warningsboth clean.