YieldStreamdocsLaunch app

Contract reference

Contract: YieldStreamDistributor · source: contracts/distributor/src/lib.rs · testnet: CC25WDHZ7SQC2UMZJHIPYM2U5ROYFXVDZMP3THMIL3IJY6HOCGGX2FPG

Signatures

FunctionReturnsAuthorized by
initialize(admin: Address)()admin
create_distribution(creator, token, merkle_root, total_amount, num_recipients, end_time)u32 (new id)creator
claim(distribution_id, index, recipient, amount, proof)()recipient
cancel(distribution_id)()campaign creator
sweep(distribution_id, to)i128 (swept)campaign creator
get_distribution(distribution_id)Distribution— view
is_claimed(distribution_id, index)bool— view
get_distribution_count()u32— view
get_admin()Address— view
compute_leaf(distribution_id, index, recipient, amount)BytesN<32>— pure
verify_proof(proof, root, leaf)bool— pure

Types

struct Distribution {
  id:             u32
  creator:        Address
  token:          Address
  merkle_root:    BytesN<32>
  total_amount:   i128
  claimed_amount: i128
  num_recipients: u32
  claimed_count:  u32
  created_at:     u64   // ledger timestamp
  end_time:       u64   // 0 = no expiration
  status:         DistributionStatus
}

enum DistributionStatus { Active = 0, Cancelled = 1, Completed = 2, Expired = 3 }

Write functions

initialize(admin)

Stores the administrator address. Callable once; a second call returns AlreadyInitialized. The stored admin is readable by get_admin but is not consulted by any other function — there is no admin-only behaviour yet (issue #9).

create_distribution(creator, token, merkle_root, total_amount, num_recipients, end_time) → u32

Allocates the next sequential id, pulls total_amount of token from creator into the contract, and writes an Active campaign record. Returns the new id.

  • total_amount must be > 0 → else InvalidAmount
  • num_recipients must be > 0 → else InvalidRecipientCount
  • end_time may be 0 (no expiry) or a future timestamp → a past value returns InvalidEndTime

The contract does not verify that the root commits to num_recipients leaves, that allocations sum to total_amount, or that token is a real token contract. Those are issuer responsibilities today and the subject of issue #7. A root that under-commits the budget leaves tokens stranded; one that over-commits lets claims exceed escrow and later claimers get reverted transfers.

claim(distribution_id, index, recipient, amount, proof)

The core path. Checks run in this order, and the order is what you will debug against:

  1. recipient.require_auth() — the claimer signs
  2. campaign must exist → DistributionNotFound
  3. amount > 0 → InvalidAmount
  4. index < num_recipients → InvalidLeafIndex
  5. leaf not already claimed → AlreadyClaimed
  6. status not Cancelled → DistributionCancelled; status must be Active → DistributionNotActive
  7. end_time == 0 or not yet passed → DistributionExpired
  8. recompute the leaf from the arguments and walk the proof to the root → InvalidProof

On success it marks the leaf claimed, increments claimed_amount and claimed_count, flips status to Completed when every leaf or every token is accounted for, transfers amount to the recipient, and emits claimed.

Note that (distribution_id, index, recipient, amount) is what the leaf commits to — the recipient cannot substitute a different recipient or amount than the issuer encoded, because the recomputed leaf would not reach the root.

cancel(distribution_id)

Creator-only. Requires status Active, sets Cancelled, and emits cancel with the remaining amount. It does not move tokens — recovery is a separate sweep call.

sweep(distribution_id, to) → i128

Creator-only. Allowed when the campaign is Cancelled or its end_time has passed; otherwise DistributionNotExpired. Transfers the remaining balance to to and returns the amount. Sweeping a Completed campaign, or one with nothing left, returns NothingToSweep.

Known defect at this version: sweep writes claimed_amount = total_amount, so a campaign that was cancelled with 80% unclaimed reads back as 100% claimed. The tokens are correct; the reported statistics are not. Tracked as issue #8.

Events

Topics are short symbols; the campaign id and the acting address are in the topic list, the payload follows as data.

TopicTopicsData
created("created", id, creator)(token, merkle_root, total_amount, end_time)
claimed("claimed", id, recipient)(index, amount)
cancel("cancel", id, creator)remaining_amount
swept("swept", id, creator)(to, swept_amount)

Error codes

Returned as ContractError and surfaced on-chain as Contract error #N in transaction results.

#NameRaised when
1NotInitializedget_admin before initialize
2AlreadyInitializedsecond initialize call
3Unauthorizedreserved — not currently raised
4InvalidAmountamount ≤ 0 on create or claim
5InvalidLeafIndexindex ≥ num_recipients
6DistributionNotFoundunknown campaign id
7DistributionNotActivestatus is not Active
8DistributionCancelledclaim against a cancelled campaign
9DistributionExpiredclaim after end_time
10DistributionNotExpiredsweep before cancel or expiry
11AlreadyClaimedleaf index already spent
12InvalidProofproof does not reach the root
13ArithmeticOverflowchecked add/sub failed
14InvalidEndTimeend_time in the past
15NothingToSweepcompleted, or zero remaining
16InvalidRecipientCountnum_recipients == 0
17ZeroAddressreserved — not currently raised

Codes 3 and 17 are defined but never returned by the current code — auth failures come from require_auth itself, not the error enum.