Skip to content

feat(distribution): exact O(1) payouts in token units, and fix the O(n^2) basis-point path - #120

Open
starknetdev wants to merge 10 commits into
mainfrom
perf/distribution-share-hoist-denominator
Open

feat(distribution): exact O(1) payouts in token units, and fix the O(n^2) basis-point path#120
starknetdev wants to merge 10 commits into
mainfrom
perf/distribution-share-hoist-denominator

Conversation

@starknetdev

@starknetdev starknetdev commented Jul 27, 2026

Copy link
Copy Markdown
Member

Two commits, in order of importance. The second is what you'd build on; the first is what keeps existing escrowed prizes safe in the meantime.

1. distribution::payout — exact payouts, O(1)

The basis-point path computes a rational number in 32.32 fixed point via Cubit pow/exp/ln, then truncates it into a u16. Both steps are avoidable. This computes the payout directly:

payout(p) = total_amount * W(p) / sum(W)

in u256 integer arithmetic, where W is an exact integer weight and sum(W) is closed form:

Distribution W(p) sum(W)
Linear(w) 10 + (n-p)*w 10n + w*n(n-1)/2
Exponential(w) (n-p+1)^k sum_{j=1..n} j^k (Faulhaber)
Uniform 1 n
Custom(shares) shares[p-1] sum(shares)

Any common scale cancels in the ratio, so the weights stay small integers. Three problems go away at once.

Cost stops depending on the size of the field

l2_gas for the winner's payout:

Paid places calculator (bps) payout (exact)
10 1,691,510 228,550
100 15,656,810 228,550
1000 155,309,810 228,550

Identical at every size, and identical for the last position as for the first. Cost tracks the shape of the weight, not the size of the field: k=3 is 367,150, Linear 273,630. Distributing all 100 places drops from ~845M l2_gas of share math to ~23M — with no calldata and no storage, unlike precomputing the curve into Custom.

Positions stop being unclaimable

A u16 basis point is 1/10000 of the pool, so a steep curve over a large field pays late positions exactly 0 — and Budokan asserts prize_amount > 0, so those players cannot claim at all. Measured first-dead-position:

Distribution Tail dies at
Exponential 2.5 20 places
Exponential 2.0 ~96 of 100
Linear 1.0 141 places

In token units the floor is 1 wei. test_no_zero_payouts_where_basis_points_die pins a 100-place k=3 curve that starves under basis points and pays every position here, and asserts the premise so it can't silently stop testing anything.

Dust stops being load-bearing

Basis-point truncation strands up to n/10000 of the pool — 0.5% of a 100-place prize, silently redirected to first place. Here the remainder is under n wei, so there is nothing worth redistributing, and payout index 1 no longer has to sum every position to find it. That is what makes the winner O(1) like everyone else, rather than the most expensive claim in the tournament.

Scope and safety

  • Additive. calculator is untouched by this commit; prizes already escrowed against basis-point math keep computing exactly as before. Adopting this is a per-prize decision in Budokan, not a global switch.
  • Curve unchanged. test_matches_the_basis_point_curve shows a 10000-unit pool reproduces calculate_share to within the 1 bps that the fixed-point implementation itself loses to rounding.
  • Fractional exponents are refused, not approximated. 1.5 and 2.5 have no closed-form power sum. supports_exact_payout reports this and calculate_payout panics rather than quietly substituting a different curve. Integer exponents are capped at 5.

On the curve shape

Exponential is a power law, not an exponential — W(p) is polynomial in the position, (n-p+1)^k, not r^p. That is precisely what makes this work: a power sum has an exact integer closed form, where geometric decay needs r^n in rationals and overflows u256 past a few dozen places. The cheapest curve to compute on-chain is the one already in use.

Worth knowing when choosing k: for a power law the winner's share is approximately (k+1)/n. Over 100 places that is ~2% at k=1 and ~6% at k=5 — the family is inherently flat at the top for large fields, and no weight fixes that. If a headline first prize matters, that wants a geometric curve or an explicit Custom array, not a steeper k.

2. calculator — the O(n^2) basis-point path

Both weighted distributions normalize a position's weight against the sum of every position's weight, and that sum was rebuilt inside every single share computation. calculate_total, calculate_dust and the winner's calculate_share_with_dust therefore re-derived the whole vector once per position.

Payout index 1 is the only index that collects dust, so winners paid it on every claim. Hoisting the weight vector takes that path from O(n^2) to O(n):

Distribution Places Before After
Exponential w10 10 10,535,820 1,691,510 6.2x
Exponential w10 50 214,644,220 7,898,310 27.2x
Exponential w10 500 20,448,538,720 77,724,810 263x
Linear w10 50 169,699,960 6,577,310 25.8x

Values are bit-identical, verified by sweeping the old and new implementations against each other across every payout index of every size 1..=12 for Linear (10/25/7), Exponential (10/15/25/100) and Uniform, at full and partial available_share, plus the zero-payouts and out-of-range edges. That matters for deployment: payouts are recomputed from stored config at claim time, so a partially-claimed tournament would over- or under-pay its remainder if the values moved.

The allocation this introduces was measured rather than assumed — it costs at most +1.4% on the one path that was never quadratic (exponential non-winners at n=1000), against 6x-263x on the path that was. See the crossover comment.

One behaviour is deliberately preserved: with total_payouts == 0, payout index 1 collects the entire available_share as dust. Budokan reaches that state with an empty leaderboard and no configured distribution_count, so changing it here would be a silent payout change. It now has a named test.

Tests

  • test_payout — conservation (never overpay; shortfall under n units), no dead positions, monotonicity, curve equivalence, exact hand-checkable weights, every Faulhaber branch against direct summation, and the guard behaviour.
  • test_share_regression — pins exact basis points for the legacy path. The existing suite asserts ranges, which would not catch a few-bps drift.
  • test_gas_benchmark — the cost curves above, including the allocation crossover and the flat payout line.

snforge test in packages/utilities: 294 passed, 0 failed. scarb build clean across the workspace, scarb fmt -w applied.

Not in this PR

  • Budokan adopting calculate_payout in _claim_distributed_prize / _claim_entry_fee_position — separate repo, and it needs a versioned prize type so escrowed prizes keep their original math.
  • Validation at add_prize rejecting a distribution whose tail rounds to zero. Worth doing regardless as a guard for prizes that stay on the basis-point path.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added exact token-unit payout calculations for uniform, linear, exponential, custom, geometric, and tiered distributions.
    • Added geometric and tiered distribution support for entry fees and prize configurations.
    • Added validation for payout types, configuration limits, and supported geometric payouts.
    • Added efficient retrieval of individual custom payout shares.
  • Bug Fixes

    • Improved weighted payout calculations and dust allocation accuracy.
    • Improved handling of empty distributions, zero pools, invalid positions, and rounding.
  • Tests

    • Added regression coverage and performance benchmarks for payout accuracy, conservation, edge cases, and distribution sizes.

Both weighted distributions normalize a position's raw weight against the
sum of every position's weight. That sum was rebuilt inside every single
share computation, so `calculate_total` — and with it `calculate_dust` and
the winner's `calculate_share_with_dust` — re-derived the whole vector once
per position: O(n^2) work, with an O(n^2) count of `FixedTrait::pow` calls
for Exponential.

Payout index 1 pays that cost on every claim, because it is the only index
that collects dust. Winners were therefore the most expensive claim in a
tournament by an order of magnitude, scaling quadratically with paid places.

Hoist the weights into a vector built once per call, and read shares,
totals and dust off it. Single-share cost is unchanged in complexity (still
O(n) — the normalization sum is inherent); the winner's path drops from
O(n^2) to O(n).

Values are unchanged. The weight expressions and their accumulation order
are preserved exactly, so the fixed-point results are bit-identical; this
was verified by sweeping the old and new implementations against each other
across every payout index of every size 1..=12 for Linear (weights 10/25/7),
Exponential (weights 10/15/25/100) and Uniform, at full and partial
available_share, plus the zero-payouts and out-of-range edges. The
zero-payout quirk where index 1 collects the entire available_share as dust
is deliberately preserved — Budokan can reach it with an empty leaderboard.

Adds `test_share_regression` pinning the exact basis points (ranges would
not catch a few-bps drift) and `test_gas_benchmark` making the cost curve
visible.

l2_gas, winner's share incl. dust:

    Exponential w10, 10 places   10,535,820 ->  1,691,510   (6.2x)
    Exponential w10, 25 places   56,570,220 ->  4,019,060  (14.1x)
    Exponential w10, 50 places  214,644,220 ->  7,898,310  (27.2x)
    Exponential w15, 10 places   77,452,060 ->  7,245,380  (10.7x)
    Exponential w15, 25 places  459,709,180 -> 18,940,140  (24.3x)
    Linear w10, 10 places         8,436,360 ->  1,426,910   (5.9x)
    Linear w10, 50 places       169,699,960 ->  6,577,310  (25.8x)

Non-winner positions are unchanged (within noise), as are Uniform and
Custom, which never normalized against a weight sum.

End to end, Budokan's `test_exponential_100_distribution_with_10_positions`
(create + 10 entries + 10 claims) drops from ~71.6M to ~52.9M l2_gas, -26%.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Changes

The distribution system adds geometric and tiered variants, exact token-unit payouts, reusable weighted-share calculations, expanded packed storage, indexed custom-share access, and regression and benchmark coverage.

Distribution payout and storage support

Layer / File(s) Summary
Distribution contracts and serialization
packages/interfaces/src/distribution.cairo, packages/utilities/src/distribution/structs.cairo, packages/metagame/src/entry_fee/entry_fee_store.cairo, packages/metagame/src/prize/structs.cairo
Geometric and tiered distributions are defined and encoded across entry-fee and prize storage.
Shared weighted calculation helpers
packages/utilities/src/distribution/calculator.cairo
Linear and exponential calculations reuse fixed-point weight vectors for shares, totals, and winner dust allocation.
Exact token-unit payout API
packages/utilities/src/distribution.cairo, packages/utilities/src/distribution/payout.cairo
The public payout module calculates integer payouts with validation, closed-form normalization, geometric bounds, and tiered settlement.
Indexed custom-share access
packages/metagame/src/prize/prize_store.cairo, packages/metagame/src/prize/prize_component.cairo, packages/metagame/src/prize/tests/mocks/prize_mock.cairo
Prize storage exposes indexed custom-share reads and no longer rebuilds the full custom share array for prize conversion.
Payout validation and benchmarks
packages/utilities/src/distribution/tests.cairo, packages/utilities/src/distribution/tests/*
Tests cover payout invariants, exact shares, dust closure, validation, geometric and tiered behavior, storage boundaries, and benchmark cases.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant EntryFeeStore
  participant Distribution
  participant calculate_payout
  participant PrizeStoreTrait
  EntryFeeStore->>Distribution: reconstruct geometric or tiered configuration
  Distribution->>calculate_payout: provide distribution and payout parameters
  calculate_payout->>calculate_payout: validate and normalize integer weights
  calculate_payout-->>Distribution: return exact token-unit payout
  PrizeStoreTrait-->>Distribution: return indexed custom share when requested
Loading

Possibly related PRs

Suggested reviewers: copilot

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the PR’s two primary changes: exact token-unit payouts and the optimized basis-point path.
Description check ✅ Passed The description thoroughly explains scope, motivation, implementation, validation, compatibility, and exclusions, but omits several template headings and checkboxes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/distribution-share-hoist-denominator

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.

@claude

claude Bot commented Jul 27, 2026

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

@github-actions

Copy link
Copy Markdown

Codex Review - General Engineering Review

Review process failed to complete.

@claude

claude Bot commented Jul 27, 2026

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

@github-actions

Copy link
Copy Markdown

Codex Review - Cairo/Starknet Contract Review

Review process failed to complete.

@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🧹 Nitpick comments (3)
packages/utilities/src/distribution/tests/test_share_regression.cairo (1)

70-110: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add function-level documentation consistently. Section headings do not document each test’s specific purpose and expected behavior.

  • packages/utilities/src/distribution/tests/test_share_regression.cairo#L70-L110: document both linear exact-share fixtures.
  • packages/utilities/src/distribution/tests/test_gas_benchmark.cairo#L34-L147: document each benchmark’s measured shape and rationale.

As per coding guidelines, “Every function must include clear explanation of what it does and why.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/utilities/src/distribution/tests/test_share_regression.cairo` around
lines 70 - 110, Add function-level documentation to both exact-share tests,
test_linear_w10_n5_exact_shares and test_linear_w25_n4_exact_shares, describing
the linear distribution fixture, expected shares, total, and dust behavior. Also
document every benchmark function in
packages/utilities/src/distribution/tests/test_gas_benchmark.cairo (lines
34-147), explaining its measured workload shape and rationale; do not rely on
section headings as substitutes for per-function documentation.

Source: Coding guidelines

packages/utilities/src/distribution/calculator.cairo (2)

156-209: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Confusing variable name: denominator_fp shadows the concept of denominator.

In the Exponential branch, denominator_fp actually holds n (the fixed-point total payout count used as the division base for each position's ratio), which is conceptually unrelated to the outer denominator accumulator (the weight-sum this function returns). Reusing the word "denominator" for two different values in the same function is confusing for future maintainers touching this formula.

♻️ Suggested rename
             let weight_fp = FixedTrait::new((weight.into() * ONE) / 10, false);
             let n_u64: u64 = n.into();
-            let denominator_fp = FixedTrait::new_unscaled(n_u64, false);
+            let n_fp = FixedTrait::new_unscaled(n_u64, false);
             let mut p: u32 = 1;
             loop {
                 if p > n {
                     break;
                 }
                 let pi: u64 = (p - 1).into();
                 let num_fp = FixedTrait::new_unscaled(n_u64 - pi, false);
-                let base_fp = num_fp / denominator_fp;
+                let base_fp = num_fp / n_fp;
                 let w = base_fp.pow(weight_fp);
                 denominator = denominator + w;
                 weights.append(w);
                 p += 1;
             }

As per path instructions, "Use descriptive variable names (e.g., liquidity_unlock_timestamp not t)."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/utilities/src/distribution/calculator.cairo` around lines 156 - 209,
Rename the Exponential branch’s denominator_fp variable to a descriptive name
representing the fixed-point total payout count used as the ratio’s division
base, and update its use in the base_fp calculation; leave the outer denominator
accumulator unchanged.

Source: Path instructions


156-243: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add structured # Arguments/# Returns docs to the new private helpers.

weight_vector has a substantial explanatory comment but no formal parameter/return documentation; share_at and sum_shares only have one-line descriptions. Every other function in this file (calculate_share, calculate_total, calculate_share_with_dust) documents arguments and return values explicitly.

As per path instructions, "Every function must include clear explanation of what it does and why, parameter descriptions with types and constraints, return value documentation, and example usage when appropriate."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/utilities/src/distribution/calculator.cairo` around lines 156 - 243,
Add complete documentation comments to the private helpers weight_vector,
share_at, and sum_shares, including # Arguments entries with types and
constraints and a # Returns entry describing each returned value and relevant
edge-case behavior. Preserve their existing explanatory descriptions and
document the 1-indexed payout_index, zero/out-of-range handling, normalization
denominator, and truncation behavior.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/utilities/src/distribution/tests/test_gas_benchmark.cairo`:
- Around line 34-39: Make bench_baseline_overhead a harness-only baseline by
removing the Distribution::Uniform setup, calculate_share invocation, and
assertion. Leave the test body empty or otherwise trivial without performing any
distribution-related work.

In `@packages/utilities/src/distribution/tests/test_share_regression.cairo`:
- Around line 27-153: The regression suite only validates full BASIS_POINTS
availability and lacks coverage for weighted boundary inputs. Extend the tests
around calculate_share, calculate_total, and calculate_share_with_dust with
locked fixtures using partial available_share values, then add fuzz/property
coverage for valid weights and payout counts, including zero-place behavior and
exact dust closure to the requested available share.

---

Nitpick comments:
In `@packages/utilities/src/distribution/calculator.cairo`:
- Around line 156-209: Rename the Exponential branch’s denominator_fp variable
to a descriptive name representing the fixed-point total payout count used as
the ratio’s division base, and update its use in the base_fp calculation; leave
the outer denominator accumulator unchanged.
- Around line 156-243: Add complete documentation comments to the private
helpers weight_vector, share_at, and sum_shares, including # Arguments entries
with types and constraints and a # Returns entry describing each returned value
and relevant edge-case behavior. Preserve their existing explanatory
descriptions and document the 1-indexed payout_index, zero/out-of-range
handling, normalization denominator, and truncation behavior.

In `@packages/utilities/src/distribution/tests/test_share_regression.cairo`:
- Around line 70-110: Add function-level documentation to both exact-share
tests, test_linear_w10_n5_exact_shares and test_linear_w25_n4_exact_shares,
describing the linear distribution fixture, expected shares, total, and dust
behavior. Also document every benchmark function in
packages/utilities/src/distribution/tests/test_gas_benchmark.cairo (lines
34-147), explaining its measured workload shape and rationale; do not rely on
section headings as substitutes for per-function documentation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 314dbdd5-8826-4b4d-bd91-855c3daa59f6

📥 Commits

Reviewing files that changed from the base of the PR and between e24bf41 and 61e6278.

📒 Files selected for processing (5)
  • packages/utilities/src/distribution.cairo
  • packages/utilities/src/distribution/calculator.cairo
  • packages/utilities/src/distribution/tests.cairo
  • packages/utilities/src/distribution/tests/test_gas_benchmark.cairo
  • packages/utilities/src/distribution/tests/test_share_regression.cairo

Comment on lines +34 to +39
#[test]
fn bench_baseline_overhead() {
let dist = Distribution::Uniform;
let share = calculate_share(dist, 1, 1, BASIS_POINTS);
assert!(share == BASIS_POINTS, "single uniform payout takes everything");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Make the baseline harness-only.

bench_baseline_overhead calls calculate_share, so subtracting it removes Uniform-distribution work as well as test-harness overhead. Use an empty or trivial test body with no distribution call.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/utilities/src/distribution/tests/test_gas_benchmark.cairo` around
lines 34 - 39, Make bench_baseline_overhead a harness-only baseline by removing
the Distribution::Uniform setup, calculate_share invocation, and assertion.
Leave the test body empty or otherwise trivial without performing any
distribution-related work.

Comment on lines +27 to +153
fn test_exponential_w10_n10_exact_shares() {
let dist = Distribution::Exponential(10);
let expected = array![1818_u16, 1636, 1454, 1272, 1090, 909, 727, 545, 363, 181];

let mut p: u32 = 1;
for e in expected {
assert!(
calculate_share(dist, p, 10, BASIS_POINTS) == e,
"exp w10 n10 position {} share changed",
p,
);
p += 1;
}

// Truncation leaves 5 bps unallocated; the winner absorbs it.
assert!(calculate_total(dist, 10, BASIS_POINTS) == 9995, "exp w10 n10 total");
assert!(
calculate_share_with_dust(dist, 1, 10, BASIS_POINTS) == 1823, "exp w10 n10 winner + dust",
);
}

/// Fractional weight — exercises the `exp(y * ln(x))` branch of `pow`.
#[test]
fn test_exponential_w15_n5_exact_shares() {
let dist = Distribution::Exponential(15);
let expected = array![3964_u16, 2836, 1842, 1002, 354];

let mut p: u32 = 1;
for e in expected {
assert!(
calculate_share(dist, p, 5, BASIS_POINTS) == e,
"exp w15 n5 position {} share changed",
p,
);
p += 1;
}

assert!(calculate_total(dist, 5, BASIS_POINTS) == 9998, "exp w15 n5 total");
assert!(
calculate_share_with_dust(dist, 1, 5, BASIS_POINTS) == 3966, "exp w15 n5 winner + dust",
);
}

#[test]
fn test_linear_w10_n5_exact_shares() {
let dist = Distribution::Linear(10);
let expected = array![3333_u16, 2666, 1999, 1333, 666];

let mut p: u32 = 1;
for e in expected {
assert!(
calculate_share(dist, p, 5, BASIS_POINTS) == e,
"linear w10 n5 position {} share changed",
p,
);
p += 1;
}

assert!(calculate_total(dist, 5, BASIS_POINTS) == 9997, "linear w10 n5 total");
assert!(
calculate_share_with_dust(dist, 1, 5, BASIS_POINTS) == 3336, "linear w10 n5 winner + dust",
);
}

#[test]
fn test_linear_w25_n4_exact_shares() {
let dist = Distribution::Linear(25);
let expected = array![4473_u16, 3157, 1842, 526];

let mut p: u32 = 1;
for e in expected {
assert!(
calculate_share(dist, p, 4, BASIS_POINTS) == e,
"linear w25 n4 position {} share changed",
p,
);
p += 1;
}

assert!(calculate_total(dist, 4, BASIS_POINTS) == 9998, "linear w25 n4 total");
assert!(
calculate_share_with_dust(dist, 1, 4, BASIS_POINTS) == 4475, "linear w25 n4 winner + dust",
);
}

/// Dust is only ever added to payout index 1, and always closes the gap to
/// `available_share` exactly.
#[test]
fn test_dust_closes_the_gap_exactly() {
let dists = array![
Distribution::Linear(10), Distribution::Linear(25), Distribution::Exponential(10),
Distribution::Exponential(15),
];

for dist in dists {
let mut n: u32 = 1;
while n <= 8 {
let mut paid: u16 = calculate_share_with_dust(dist, 1, n, BASIS_POINTS);
let mut p: u32 = 2;
while p <= n {
paid += calculate_share_with_dust(dist, p, n, BASIS_POINTS);
p += 1;
}
assert!(paid == BASIS_POINTS, "shares + dust must total 100% at n={}", n);
n += 1;
}
};
}

/// A zero-position distribution hands the whole share to payout index 1 as
/// dust. Preserved deliberately from the pre-hoist implementation — callers
/// (Budokan's `_claim_distributed_prize`) can reach this with an empty
/// leaderboard and no configured `distribution_count`.
#[test]
fn test_zero_payouts_gives_everything_to_index_one_as_dust() {
let dists = array![Distribution::Linear(10), Distribution::Exponential(10)];
for dist in dists {
assert!(
calculate_share(dist, 1, 0, BASIS_POINTS) == 0, "no share when there are no places",
);
assert!(calculate_total(dist, 0, BASIS_POINTS) == 0, "no total when there are no places");
assert!(
calculate_share_with_dust(dist, 1, 0, BASIS_POINTS) == BASIS_POINTS,
"index 1 absorbs the full share as dust",
);
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Repository files matching target:"
fd -a 'test_share_regression\.cairo|distribution' . | sed 's#^\./##' | head -100

echo
echo "Target file size and content outline:"
if [ -f packages/utilities/src/distribution/tests/test_share_regression.cairo ]; then
  wc -l packages/utilities/src/distribution/tests/test_share_regression.cairo
  ast-grep outline packages/utilities/src/distribution/tests/test_share_regression.cairo --view compact || true
  echo
  echo "Relevant file section:"
  cat -n packages/utilities/src/distribution/tests/test_share_regression.cairo | sed -n '1,220p'
else
  echo "target file not found"
fi

echo
echo "Search for partial available_share tests/fixtures/dust/coverage:"
rg -n --hidden "available_share|BASIS_POINTS|fuzz|test_fuzz|fn fuzz|partial|coverage|coverage|dust|weighted|calculate_share_with_dust|test_dust|calculate_total|Exponential|Linear" packages/utilities -S | sed -n '1,220p'

Repository: Provable-Games/game-components

Length of output: 35499


Cover partial availability and fuzz weighted boundaries.

The regression title documents full/partial available_share, but every assertion in this suite still uses BASIS_POINTS, so partial-share regressions remain unprotected. Add locked partial-share fixtures and fuzz valid weighted inputs, including zero-place behavior and dust closure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/utilities/src/distribution/tests/test_share_regression.cairo` around
lines 27 - 153, The regression suite only validates full BASIS_POINTS
availability and lacks coverage for weighted boundary inputs. Extend the tests
around calculate_share, calculate_total, and calculate_share_with_dust with
locked fixtures using partial available_share values, then add fuzz/property
coverage for valid weights and payout counts, including zero-place behavior and
exact dust closure to the requested available share.

Source: Coding guidelines

…iling

The hoist trades `pow` calls for an `Array<Fixed>` the per-position
implementation never allocated. Paths that were not quadratic get that
allocation for nothing, so measure where it stops paying instead of
assuming it always does.

Adds benchmarks at n = 1/2/3 (where n^2 and n are the same order), at
n = 100/200/1000 for non-winner positions (allocation against no saving),
and at n = 500/1000 for winners, plus a Custom n=1000 comparison point.

Measured, l2_gas:

  Winner (pos 1)          before          after      ratio
    n=1                  486,780        294,980      1.7x
    n=2                  952,540        450,150      2.1x
    n=3                1,581,000        605,320      2.6x
    n=500         20,448,538,720     77,724,810      263x
    n=1000        did not complete    155,309,810      -

  Non-winner (pos n)      before          after      delta
    n=3                  407,990        373,370      -8.5%
    n=10 exp             977,440        951,220      -2.7%
    n=10 linear          786,580        686,620     -12.7%
    n=50               4,231,440      4,253,220      +0.5%
    n=100              8,298,940      8,380,720      +1.0%
    n=200 exp         16,433,940     16,635,720      +1.2%
    n=1000 exp        81,513,940     82,675,720      +1.4%

So the allocation is never the dominant term. The winner path wins at
every size including n=1. Exponential non-winners cross over around
n=25 and pay at most +1.4% at n=1000 — set against 6x-263x on the path
that actually blocked claims. Linear non-winners stay ahead throughout
(the old code computed its weight n+1 times, the vector computes it n).

Also documents a precision ceiling found while benchmarking, which is a
property of the u16 basis-point representation and predates this change
(values are bit-identical before and after): a position whose normalized
weight falls below 1/10000 of the pool truncates to a 0 bps share, and
Budokan asserts `prize_amount > 0`, so that position can never be
claimed. Last place hits zero at n=141 for both Linear(10) and
Exponential(10), and at n=20 for the steeper Exponential(25). The
n200/n1000 tail benchmarks assert bounds rather than non-zero for this
reason.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@starknetdev

Copy link
Copy Markdown
Member Author

Allocation crossover — measured, not assumed

Flagged in review that the new Array<Fixed> is overhead the per-position implementation never paid, and could in principle cost more than the compute it removes. It can, but only in one place and only marginally. Benchmarks added in 338a490.

Winner path (payout index 1) — wins at every size

n Before After
1 486,780 294,980 1.7×
2 952,540 450,150 2.1×
3 1,581,000 605,320 2.6×
10 10,535,820 1,691,510 6.2×
50 214,644,220 7,898,310 27.2×
500 20,448,538,720 77,724,810 263×
1000 did not complete within 4e9 steps 155,309,810

No small-n regression: even at n=1 the old path ran the share computation twice (once directly, once through calculate_dust), so the vector is ahead immediately.

Non-winner path (payout index n) — where allocation is pure overhead

n Before After Delta
3 407,990 373,370 −8.5%
10 (exp) 977,440 951,220 −2.7%
10 (linear) 786,580 686,620 −12.7%
50 (exp) 4,231,440 4,253,220 +0.5%
100 (exp) 8,298,940 8,380,720 +1.0%
200 (exp) 16,433,940 16,635,720 +1.2%
200 (linear) 12,963,680 11,353,220 −12.4%
1000 (exp) 81,513,940 82,675,720 +1.4%

Exponential non-winners cross over around n=25 and top out at +1.4% at n=1000. That is the entire downside, set against 6×–263× on the path that was actually making claims expensive. Linear non-winners stay ahead at every size, because the old code computed each weight n+1 times where the vector computes it n.

Scaling to 1000+ places

Cost is now linear at ~155k l2_gas per position (n=500 → 77.7M, n=1000 → 155.3M, exactly 2×). The old implementation could not compute a 1000-place winner's share at all within 4e9 steps.

But gas is not the binding constraint at that size. Shares are u16 basis points, so a position whose normalized weight falls below 1/10000 of the pool truncates to 0 — and Budokan asserts prize_amount > 0, so that position can never be claimed. Measured first-zero-tail:

Distribution Last place hits 0 bps at
Linear(10) n = 141
Exponential(10) n = 141
Exponential(25) n = 20

This predates this PR — values are bit-identical before and after — but it means a weighted prize with more than ~140 places (or ~20 for a steep exponential) already has unclaimable tail positions today. Worth a validation at add_prize time in Budokan rather than a surprise at claim time. Not fixed here.

@claude

claude Bot commented Jul 27, 2026

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

@github-actions

Copy link
Copy Markdown

Codex Review - General Engineering Review

Review process failed to complete.

@claude

claude Bot commented Jul 27, 2026

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

@github-actions

Copy link
Copy Markdown

Codex Review - Cairo/Starknet Contract Review

Review process failed to complete.

100 places is a shape we want to support, so pin what it costs and what
it costs with the curve precomputed off-chain instead.

  l2_gas, 100 places        Exponential w10    Custom
    winner (pos 1)               15,656,810     4,740,820
    other  (pos n)                8,380,720     1,097,550

Every position pays O(n) because each claim rebuilds the weight vector,
so paying out all 100 costs ~845M l2_gas of share math (15.7M + 99 x
8.4M) versus ~113M for Custom — before any transfer or storage cost.
Custom needs 7 packed felt slots for 100 shares and does no `pow` at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Jul 27, 2026

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

@github-actions

Copy link
Copy Markdown

Codex Review - General Engineering Review

Review process failed to complete.

@claude

claude Bot commented Jul 27, 2026

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

@github-actions

Copy link
Copy Markdown

Codex Review - Cairo/Starknet Contract Review

Review process failed to complete.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/utilities/src/distribution/tests/test_gas_benchmark.cairo (1)

130-225: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document each added benchmark function.

The group headings explain categories, but the individual benchmark functions lack the required explanation of their specific shape and purpose. Add concise Cairo doc comments where the function name alone is insufficient.

As per coding guidelines, “Every function must include clear explanation of what it does and why.”

Also applies to: 272-303

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/utilities/src/distribution/tests/test_gas_benchmark.cairo` around
lines 130 - 225, The added benchmark functions in the distribution benchmark
suite lack function-level documentation. Add concise Cairo doc comments to each
benchmark, including the exponential and linear tail cases, the large-count
ceiling cases, and the custom-distribution comparison, describing the tested
shape and its purpose; retain the existing group headings and assertions.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/utilities/src/distribution/tests/test_gas_benchmark.cairo`:
- Around line 217-223: Separate fixture construction from the metered
distribution calculation in the benchmark tests. At
packages/utilities/src/distribution/tests/test_gas_benchmark.cairo lines
217-223, isolate or subtract the 1,000-entry Custom-share setup; apply the same
treatment to the 100-entry winner fixture at lines 281-287 and the 100-entry
tail fixture at lines 294-300. Alternatively, explicitly label these benchmarks
as measuring construction plus calculation rather than the claimed
calculation-only path.

---

Nitpick comments:
In `@packages/utilities/src/distribution/tests/test_gas_benchmark.cairo`:
- Around line 130-225: The added benchmark functions in the distribution
benchmark suite lack function-level documentation. Add concise Cairo doc
comments to each benchmark, including the exponential and linear tail cases, the
large-count ceiling cases, and the custom-distribution comparison, describing
the tested shape and its purpose; retain the existing group headings and
assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d6fc96ab-2b79-422f-aa2c-36c0eb967c9e

📥 Commits

Reviewing files that changed from the base of the PR and between 61e6278 and cc04cfe.

📒 Files selected for processing (1)
  • packages/utilities/src/distribution/tests/test_gas_benchmark.cairo

Comment on lines +217 to +223
let mut shares: Array<u16> = array![];
let mut i: u32 = 0;
while i < 1000 {
shares.append(10);
i += 1;
}
let dist = Distribution::Custom(shares.span());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Separate Custom input construction from calculation gas.

These tests build the entire Array<u16> inside the metered body, so their reported cost includes O(n) fixture allocation rather than just the claimed O(1) Custom share path. Measure/subtract equivalent setup cost, or label these as end-to-end construction-plus-calculation benchmarks.

  • packages/utilities/src/distribution/tests/test_gas_benchmark.cairo#L217-L223: isolate the 1,000-entry Custom-share construction cost.
  • packages/utilities/src/distribution/tests/test_gas_benchmark.cairo#L281-L287: isolate the 100-entry winner fixture construction cost.
  • packages/utilities/src/distribution/tests/test_gas_benchmark.cairo#L294-L300: isolate the 100-entry tail fixture construction cost.
📍 Affects 1 file
  • packages/utilities/src/distribution/tests/test_gas_benchmark.cairo#L217-L223 (this comment)
  • packages/utilities/src/distribution/tests/test_gas_benchmark.cairo#L281-L287
  • packages/utilities/src/distribution/tests/test_gas_benchmark.cairo#L294-L300
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/utilities/src/distribution/tests/test_gas_benchmark.cairo` around
lines 217 - 223, Separate fixture construction from the metered distribution
calculation in the benchmark tests. At
packages/utilities/src/distribution/tests/test_gas_benchmark.cairo lines
217-223, isolate or subtract the 1,000-entry Custom-share setup; apply the same
treatment to the 100-entry winner fixture at lines 281-287 and the 100-entry
tail fixture at lines 294-300. Alternatively, explicitly label these benchmarks
as measuring construction plus calculation rather than the claimed
calculation-only path.

Adds `distribution::payout`, computing a position's payout directly:

    payout(p) = total_amount * W(p) / sum(W)

in u256 integer arithmetic, where W is an exact integer weight and sum(W)
is closed form. The basis-point path in `calculator` stays as-is for
prizes already escrowed against it.

Three problems go away at once.

**Cost stops depending on the size of the field.** No loop, no allocation,
no Cubit `pow`. Measured l2_gas, winner's payout:

    places        calculator      payout
    10             1,691,510     228,550
    100           15,656,810     228,550
    1000         155,309,810     228,550

Identical at every size, and identical for the last position as for the
first (k=3 costs 367,150; Linear 273,630 — the shape of the weight, not
the size of the field). Distributing 100 places drops from ~845M l2_gas
of share math to ~23M.

**Positions stop being unclaimable.** A u16 basis point is 1/10000 of the
pool, so a steep curve over a large field gives late positions exactly 0
— and Budokan asserts `prize_amount > 0`, so those players cannot claim
at all. Measured: Exponential(2.5) loses the tail from 20 places,
Linear(1.0) from 141. In token units the floor is 1 wei;
`test_no_zero_payouts_where_basis_points_die` pins a 100-place k=3 curve
that starves under basis points and pays everyone here.

**Dust stops being load-bearing.** Basis-point truncation strands up to
n/10000 of the pool — 0.5% of a 100-place prize, silently redirected to
first place. Here the remainder is under n wei, so there is nothing worth
redistributing, and payout index 1 no longer has to sum every position to
find it. That is what makes the winner O(1) like everyone else.

Weights (any common scale cancels, so they stay small integers):

    Linear(w)       W(p) = 10 + (n-p)*w    sum = 10n + w*n(n-1)/2
    Exponential(w)  W(p) = (n-p+1)^k       sum = Faulhaber(k, n)
    Uniform         W(p) = 1               sum = n
    Custom(shares)  W(p) = shares[p-1]     sum = sum(shares)

`Exponential` is a power law rather than a true exponential — W(p) is
polynomial in the position. That is what makes this possible: a power sum
has an exact integer closed form, where geometric decay would need r^n in
rationals and overflows u256 past a few dozen places.

Fractional exponents (1.5, 2.5) have no closed-form power sum.
`supports_exact_payout` reports them, and `calculate_payout` panics rather
than quietly substituting a different curve; those distributions stay on
the basis-point path. Integer exponents are capped at 5 — a k=3 curve
over 100 places already pays first place a million times last.

Curve values are unchanged: `test_matches_the_basis_point_curve` shows a
10000-unit pool reproduces `calculate_share` to within the 1 bps the
fixed-point implementation itself loses to rounding.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Jul 27, 2026

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

@github-actions

Copy link
Copy Markdown

Codex Review - General Engineering Review

Review process failed to complete.

@claude

claude Bot commented Jul 27, 2026

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

@github-actions

Copy link
Copy Markdown

Codex Review - Cairo/Starknet Contract Review

Review process failed to complete.

@starknetdev starknetdev changed the title perf(distribution): compute the weight sum once instead of per position feat(distribution): exact O(1) payouts in token units, and fix the O(n^2) basis-point path Jul 27, 2026
Brings the branch up to date with v1.1.11 and, more usefully, onto the
org-shared AI review workflow adopted in #122 — this branch predates it and
was still running the retired hand-rolled review jobs.
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

GPT Code Review

Review execution failed (exit code 1).

The Codex CLI log was not posted because it can contain the full review prompt.
See the workflow run for job status: https://github.com/Provable-Games/game-components/actions/runs/31015917686

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

Claude Code Review

I have enough to write the review. Let me confirm the Fixed unused import concern is not valid (it's used) and finalize.

[HIGH] packages/metagame/src/entry_fee/entry_fee_store.cairo:252 - Geometric/Tiered ratio packing a * 256 + b is not validated against the 255 cap that supports_exact_payout relies on. A caller can persist a config with b > 255 (e.g. Geometric((250, 300))).
Impact: 250*256+300 = 64300 fits u16, so no panic on store, but unpack (param / 256, param % 256) reads back (251, 44) — the stored distribution is silently corrupted, and every entry-fee payout is then computed from the wrong curve. Values in 128..=255 for b all corrupt; a > 255 overflows u16 and reverts instead.
Fix: gate the store path on the same caps supports_exact_payout enforces before packing, e.g.

Distribution::Geometric((a, b)) => {
    assert!(*a <= 255 && *b <= 255, "Distribution: geometric ratio terms must fit one byte");
    (DIST_TYPE_GEOMETRIC, *a * 256 + *b, 0_u16, 0_u16)
},

and likewise for Tiered's head_ratio.

[HIGH] packages/metagame/src/prize/structs.cairo:148 - Same unvalidated a * 256 + b packing for Geometric (line 148) and Tiered (line 151) in pack_token_type.
Impact: An ERC20 prize configured with a geometric/tiered ratio term in 128..=255 round-trips to a different ratio through PackedERC20Data, so escrowed prizes pay out on a curve the sponsor never chose — with no error at add_prize time. a > 255 reverts on the u16 multiply.
Fix: assert a <= 255 && b <= 255 before packing in both arms, mirroring the supports_exact_payout contract that the storage layout depends on.

[LOW] packages/metagame/src/prize/structs.cairo:47 - The PackedERC20Data doc block is stale after widening: it still states "184 bits total" and the high layout payout_type(8) | param(16) | count(32), omitting the new param2(16) | param3(16) (actual total 216 bits).
Impact: Misleads future maintainers about the packed layout and remaining free bits above bit 88.
Fix: update the comment to 216 bits and add | param2(16) | param3(16) to the high-u128 layout line.

Note on the geometric overflow envelope: max_geometric_payouts guarantees a^(n-1) < 2^128, so total_amount * W(1) only stays within u256 when total_amount fits u128. calculate_payout takes total_amount: u256 (payout.cairo), so the guarantee rests on every caller passing a u128-bounded pool. Prize/entry-fee storage keeps amount as u128 so current callers are safe — flagging only so the u128 precondition is documented at the calculate_payout signature rather than implied.

Addresses the [MEDIUM] finding from the Claude review on #120.

`payout_weight` yields 0 for a position past `total_payouts`, but
`payout_weight_sum` summed the entire shares array. The two parameters are
independent — a caller may pay fewer places than the stored curve describes —
so whenever `total_payouts < shares.len()` the denominator carried weight for
positions that never get paid. Every payout came out proportionally short and
the difference was stranded: the truncated tail's full weight, not the
sub-unit rounding the module documents.

With shares [5000, 3000, 2000] paying 2 places from a 1000 pool, that was
500 + 300 = 800 paid of 1000, with 200 stuck. Now 625 + 375 = 1000.

Not reachable from Budokan, which validates `shares.len() == distribution_count`
and passes that same count as `total_payouts` — but this is a library function
and the invariant belongs here, not in the caller.

Also documents the `Exponential` overflow envelope on `calculate_payout`
([LOW] from the same review). The largest intermediate is `total_amount * n^k`;
against a 10^27 pool the ceiling is k=18 at 100 places, k=15 at 1,000 and k=13
at 10,000. `MAX_EXACT_EXPONENT` is 5, so every accepted curve is far inside it
— the bound only matters if that cap is ever raised.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🧹 Nitpick comments (5)
packages/utilities/src/distribution/payout.cairo (1)

99-99: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Define the error messages as constants.

The panic message at line 99 and the assertion message at line 195 are inline literals. The coding guidelines require descriptive error constants. Extract the static text into module-level constants and keep only the formatted value inline.

As per coding guidelines: "All error messages must be implemented as descriptive constants".

Also applies to: 193-196

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/utilities/src/distribution/payout.cairo` at line 99, Extract the
static panic and assertion message text in the payout logic into descriptive
module-level constants, then update the panic near the exponent range check and
the assertion around the later validation to reference those constants while
keeping only the formatted value inline.

Source: Coding guidelines

packages/utilities/src/distribution/tests/test_payout.cairo (1)

37-52: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add fuzz coverage for payout conservation.

test_payouts_never_exceed_the_pool only exercises fixed Distributions and n <= 40. Since edge cases must be fuzzed, add a #[test] #[fuzzer] function parameterized by fuzzable parameters, then asserts paid <= POOL and POOL - paid < total_payouts.into().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/utilities/src/distribution/tests/test_payout.cairo` around lines 37
- 52, Add a fuzz-enabled test alongside test_payouts_never_exceed_the_pool,
parameterized with fuzzable Distribution and payout-count inputs, and call
sum_payouts with the fuzzed values and POOL. Assert both conservation
conditions: paid <= POOL and POOL - paid < total_payouts.into(), using the
fuzzed payout count rather than the fixed n loop.

Source: Coding guidelines

packages/utilities/src/distribution/tests/test_gas_benchmark.cairo (3)

317-321: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add function-level documentation to the new payout benchmarks.

The new bench_payout_* functions have no function-level explanation. Document each benchmark’s purpose. State that these tests take no parameters and return no value.

As per coding guidelines: “Every function must include clear explanation of what it does and why, parameter descriptions with types and constraints, return value documentation, and example usage when appropriate.”

Also applies to: 323-327, 329-333, 335-339, 341-345, 347-351, 353-357

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/utilities/src/distribution/tests/test_gas_benchmark.cairo` around
lines 317 - 321, Add function-level documentation to each new bench_payout_*
benchmark, explaining its purpose and why it exercises the corresponding payout
scenario. Document that these functions take no parameters and return no value,
including relevant constraints or example usage where appropriate, without
changing benchmark behavior.

Source: Coding guidelines


318-320: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use a descriptive name for the payout result.

Each new benchmark stores the token-unit result in p. Rename it to payout_amount so its unit and meaning are clear.

As per coding guidelines: “Use descriptive variable names (e.g., liquidity_unlock_timestamp not t).”

Also applies to: 324-326, 330-332, 336-338, 342-344, 348-350, 354-356

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/utilities/src/distribution/tests/test_gas_benchmark.cairo` around
lines 318 - 320, Rename the payout result variable from p to payout_amount in
bench_payout_exp_k1_n10_pos1 and all additionally referenced benchmark blocks,
updating each assertion to use the descriptive name while preserving the
existing calculations and checks.

Source: Coding guidelines


5-8: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Correct the module statement about value assertions.

Lines 5-8 say that these tests assert nothing about values. The file asserts share > 0, share <= BASIS_POINTS, and p > 0. State that the tests use coarse sanity assertions and that the correctness suite owns exact value checks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/utilities/src/distribution/tests/test_gas_benchmark.cairo` around
lines 5 - 8, Update the module-level comment in the gas benchmark tests to state
that they use coarse sanity assertions, including basic bounds or positivity
checks, while the correctness suite in calculator.cairo owns exact value
validation. Keep the existing explanation of benchmark purpose and compared gas
figures unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/utilities/src/distribution/payout.cairo`:
- Around line 115-128: Validate the distribution at the start of both public
helpers, payout_weight and payout_weight_sum, by applying supports_exact_payout
before performing weight conversion or exponentiation. Reject unsupported
exponential weights such as 0 and 15 rather than silently truncating or entering
the power-sum panic path, while preserving the existing calculations for
supported distributions.
- Around line 164-174: Update the Custom branch of payout_weight_sum to sum only
the first total_payouts entries of shares, preventing weights for unpaid
positions from entering the denominator. Review calculate_payout’s handling of
mismatched shares.len() and total_payouts, asserting equality if mismatches are
invalid, and extend test_custom_uses_its_explicit_shares with cases where
total_payouts is smaller and larger than shares.len().

---

Nitpick comments:
In `@packages/utilities/src/distribution/payout.cairo`:
- Line 99: Extract the static panic and assertion message text in the payout
logic into descriptive module-level constants, then update the panic near the
exponent range check and the assertion around the later validation to reference
those constants while keeping only the formatted value inline.

In `@packages/utilities/src/distribution/tests/test_gas_benchmark.cairo`:
- Around line 317-321: Add function-level documentation to each new
bench_payout_* benchmark, explaining its purpose and why it exercises the
corresponding payout scenario. Document that these functions take no parameters
and return no value, including relevant constraints or example usage where
appropriate, without changing benchmark behavior.
- Around line 318-320: Rename the payout result variable from p to payout_amount
in bench_payout_exp_k1_n10_pos1 and all additionally referenced benchmark
blocks, updating each assertion to use the descriptive name while preserving the
existing calculations and checks.
- Around line 5-8: Update the module-level comment in the gas benchmark tests to
state that they use coarse sanity assertions, including basic bounds or
positivity checks, while the correctness suite in calculator.cairo owns exact
value validation. Keep the existing explanation of benchmark purpose and
compared gas figures unchanged.

In `@packages/utilities/src/distribution/tests/test_payout.cairo`:
- Around line 37-52: Add a fuzz-enabled test alongside
test_payouts_never_exceed_the_pool, parameterized with fuzzable Distribution and
payout-count inputs, and call sum_payouts with the fuzzed values and POOL.
Assert both conservation conditions: paid <= POOL and POOL - paid <
total_payouts.into(), using the fuzzed payout count rather than the fixed n
loop.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 153939c8-4a20-4c61-ba41-54d7f6c91ae6

📥 Commits

Reviewing files that changed from the base of the PR and between cc04cfe and d5e8e4f.

📒 Files selected for processing (5)
  • packages/utilities/src/distribution.cairo
  • packages/utilities/src/distribution/payout.cairo
  • packages/utilities/src/distribution/tests.cairo
  • packages/utilities/src/distribution/tests/test_gas_benchmark.cairo
  • packages/utilities/src/distribution/tests/test_payout.cairo
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/utilities/src/distribution/tests.cairo

Comment thread packages/utilities/src/distribution/payout.cairo Outdated
Comment thread packages/utilities/src/distribution/payout.cairo
starknetdev and others added 2 commits August 5, 2026 03:56
Addresses the [LOW] finding from the Claude review on #120.

`payout_weight` and `payout_weight_sum` were `pub` but do no
`supports_exact_payout` gating, and `Exponential` derives `k = weight / 10` by
integer truncation. An external caller passing `Exponential(15)` would have
silently received the k=1 curve instead of the k=1.5 one — a different
distribution, no panic. `calculate_payout` guards correctly, so making it the
only public entry point closes the hole rather than duplicating the assert.

Tests live in the same crate and are unaffected. Also corrects the module
doc table, which still described `Custom`'s denominator as the whole shares
array after the previous commit bounded it by the paid-place count.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…field size

Prototype for review, not a commitment. `Exponential` is a power law, so its
winner share thins as roughly (k+1)/n: 45% over 10 places, 5.8% over 100, and
no weight fixes that. A headline first prize on a large field needs geometric
decay, which the current set cannot express at any setting.

`Geometric(a, b)` gives each place `b / a` of the one above:

    W(p) = a^(n-p) * b^(p-1)      sum = (a^n - b^n) / (a - b)

Exact in integers — no fixed point — and the winner takes about `1 - b/a`
regardless of n:

  ratio (10,7), first place:   30.9% at n=10, 30.0% at n=39
  Exponential k=5, first place: 45.3% at n=10,  5.8% at n=100

Three things the prototype surfaced that are worth knowing before committing
to the enum change:

1. **The packed storage only carries one u16 per distribution.** Both
   `entry_fee_store` and `prize/structs` pack `(u8 tag, u16 param)`, so a
   two-term ratio does not fit. Rather than widen the layout — a storage
   change for every consumer — `a` and `b` are capped at 255 and packed as
   `a * 256 + b`. That is still every ratio with terms under 256, far more
   precision than a decay setting needs, and the layout is untouched.

2. **Reach is bounded, and tightens as the ratio gets finer.** The heaviest
   weight is `a^(n-1)`; holding it under 2^128 keeps `pool * W(1)` inside u256
   for any u128 pool, with no pool-size caveat. That gives 129 places at (2,1),
   81 at (3,2), 46 at (7,5), 39 at (10,7). `calculate_payout` asserts it rather
   than overflowing, and `max_geometric_payouts` lets a caller reject at
   creation. A host wanting more places picks a coarser ratio for the same
   decay.

3. **It is not as cheap as the power law.** Its exponent scales with the field
   instead of being capped at MAX_EXACT_EXPONENT, so it costs O(log n) large
   u256 multiplications. `int_pow` is now exponentiation by squaring, which
   took a 39-place winner from 6.9M to 3.4M l2_gas — still ~15x the power
   law's ~230k, flat across position. Against Custom at the same reach that is
   a bargain; against Exponential it is a real cost for a shape you cannot
   otherwise have.

`calculator` (the basis-point path) panics for Geometric rather than
approximating it — a geometric tail dies under basis points even worse than a
steep power law, so there is no honest bps form to offer.

The enum variant is appended, not inserted: Serde indices are positional, and
inserting earlier would reinterpret every stored and indexed distribution.

snforge: 301 passed. The 2 failures are the pre-existing local step-limit ones
on the basis-point comparison tests, which pass in CI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/utilities/src/distribution/tests/test_payout.cairo (2)

212-230: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Test the zero-exponent rejection.

supports_exact_payout has a separate w != 0 guard in packages/utilities/src/distribution/payout.cairo lines 121-125. These tests cover fractional and excessive exponents, but not Distribution::Exponential(0). Add an assertion for rejection and a calculate_payout panic test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/utilities/src/distribution/tests/test_payout.cairo` around lines 212
- 230, Extend the exponent validation tests around supports_exact_payout and
calculate_payout to cover Distribution::Exponential(0). Assert that
supports_exact_payout rejects the zero exponent, and add a should_panic test
confirming calculate_payout uses the expected rejection message.

187-200: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use an independent Faulhaber reference.

Line 197 sums payout_weight, which is production logic. This only verifies that payout_weight_sum agrees with payout_weight. A shared rank or exponent defect can pass this test. Accumulate (n - p + 1)^k with test-local repeated multiplication before comparison.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/utilities/src/distribution/tests/test_payout.cairo` around lines 187
- 200, Update test_power_sums_match_direct_summation to use an independent
Faulhaber-style reference instead of summing payout_weight inside the test. Keep
the outer loops over k and n, but build reference by computing (n - p + 1)^k
with test-local repeated multiplication for each p, then compare that
independent total against payout_weight_sum. Do not rely on payout_weight in
this test so the check remains valid even if production ranking/exponent logic
is wrong.
🧹 Nitpick comments (2)
packages/utilities/src/distribution/payout.cairo (1)

99-135: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Complete documentation for the new calculation functions.

The new documentation explains formulas, but it does not consistently document parameter types and constraints or return values. Add # Arguments and # Returns sections. Add examples for public APIs where useful.

  • packages/utilities/src/distribution/payout.cairo#L99-L135: document max_geometric_payouts and supports_exact_payout.
  • packages/utilities/src/distribution/payout.cairo#L141-L178: document power_sum and int_pow.
  • packages/utilities/src/distribution/payout.cairo#L183-L282: document payout_weight and payout_weight_sum.
  • packages/utilities/src/distribution/payout.cairo#L314-L341: complete calculate_payout parameter and return documentation.
  • packages/utilities/src/distribution/calculator.cairo#L148-L254: document weight_vector, share_at, and sum_shares.

As per coding guidelines, every function must include purpose, parameter types and constraints, return-value documentation, and example usage when appropriate.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/utilities/src/distribution/payout.cairo` around lines 99 - 135,
Complete the documentation for max_geometric_payouts and supports_exact_payout
in packages/utilities/src/distribution/payout.cairo lines 99-135 by adding
purpose, typed parameter constraints, return-value descriptions, and useful
examples; apply the same documentation requirements to power_sum and int_pow in
packages/utilities/src/distribution/payout.cairo lines 141-178, payout_weight
and payout_weight_sum in lines 183-282, and complete calculate_payout’s
parameter and return documentation in lines 314-341. Also document
weight_vector, share_at, and sum_shares in
packages/utilities/src/distribution/calculator.cairo lines 148-254 with purpose,
parameter types and constraints, return values, and examples where appropriate.

Source: Coding guidelines

packages/utilities/src/distribution/tests/test_payout.cairo (1)

283-369: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document each new geometric test function.

The section comment does not document each test invariant or boundary. Add /// documentation to every new geometric test and benchmark function. State the tested condition, expected result, and reason for the boundary.

As per coding guidelines, “Every function must include clear explanation of what it does and why.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/utilities/src/distribution/tests/test_payout.cairo` around lines 283
- 369, Add /// documentation to each new geometric test and benchmark function
in the shown section: test_geometric_ratio_holds_between_adjacent_positions,
test_geometric_winner_share_is_independent_of_field_size,
test_geometric_conserves_the_pool,
test_max_geometric_payouts_matches_the_documented_reach,
test_geometric_beyond_reach_is_refused_not_overflowed,
test_geometric_shape_guards, bench_payout_geometric_n39_pos1, and
bench_payout_geometric_n39_pos_last. Describe each function’s condition,
expected outcome, and the rationale for its boundary or invariant.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/metagame/src/prize/structs.cairo`:
- Line 27: Update test_fuzz_realistic_payout_types to treat
PAYOUT_TYPE_GEOMETRIC as valid by expanding the generated payout-type domain
from 0–4 to 0–5. Add an API-level packing/unpacking round-trip that covers
Distribution::Geometric((10, 7)), preserving the existing round-trip coverage
for other distribution variants.

In `@packages/utilities/src/distribution/structs.cairo`:
- Around line 18-21: Update the packed-layout documentation near
DIST_TYPE_GEOMETRIC to include type 4 as the geometric distribution and describe
dist_param as storing a * 256 + b for it; restrict the zero-value statement to
the remaining distribution types.

In `@packages/utilities/src/distribution/tests/test_payout.cairo`:
- Around line 304-311: Rename
test_geometric_winner_share_is_independent_of_field_size to reflect finite-field
normalization depending on field size, and update the comment above the
calculations to state that the winner share approaches 1 - b/a as n increases
rather than claiming independence. Keep the existing assertions and payout
calculations unchanged.
- Around line 283-355: Add #[fuzzer] tests alongside
test_geometric_conserves_the_pool covering valid a,b ratios accepted by
supports_exact_payout, field sizes at and around max_geometric_payouts(a),
payout indices including first, last, and boundary values, and varied pool
sizes. For every valid generated input, verify payouts do not exceed the pool
and total shortfall remains below n units, while preserving existing fixed-case
tests and confirming snforge test --coverage passes without regression.

---

Outside diff comments:
In `@packages/utilities/src/distribution/tests/test_payout.cairo`:
- Around line 212-230: Extend the exponent validation tests around
supports_exact_payout and calculate_payout to cover
Distribution::Exponential(0). Assert that supports_exact_payout rejects the zero
exponent, and add a should_panic test confirming calculate_payout uses the
expected rejection message.
- Around line 187-200: Update test_power_sums_match_direct_summation to use an
independent Faulhaber-style reference instead of summing payout_weight inside
the test. Keep the outer loops over k and n, but build reference by computing (n
- p + 1)^k with test-local repeated multiplication for each p, then compare that
independent total against payout_weight_sum. Do not rely on payout_weight in
this test so the check remains valid even if production ranking/exponent logic
is wrong.

---

Nitpick comments:
In `@packages/utilities/src/distribution/payout.cairo`:
- Around line 99-135: Complete the documentation for max_geometric_payouts and
supports_exact_payout in packages/utilities/src/distribution/payout.cairo lines
99-135 by adding purpose, typed parameter constraints, return-value
descriptions, and useful examples; apply the same documentation requirements to
power_sum and int_pow in packages/utilities/src/distribution/payout.cairo lines
141-178, payout_weight and payout_weight_sum in lines 183-282, and complete
calculate_payout’s parameter and return documentation in lines 314-341. Also
document weight_vector, share_at, and sum_shares in
packages/utilities/src/distribution/calculator.cairo lines 148-254 with purpose,
parameter types and constraints, return values, and examples where appropriate.

In `@packages/utilities/src/distribution/tests/test_payout.cairo`:
- Around line 283-369: Add /// documentation to each new geometric test and
benchmark function in the shown section:
test_geometric_ratio_holds_between_adjacent_positions,
test_geometric_winner_share_is_independent_of_field_size,
test_geometric_conserves_the_pool,
test_max_geometric_payouts_matches_the_documented_reach,
test_geometric_beyond_reach_is_refused_not_overflowed,
test_geometric_shape_guards, bench_payout_geometric_n39_pos1, and
bench_payout_geometric_n39_pos_last. Describe each function’s condition,
expected outcome, and the rationale for its boundary or invariant.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: af6dece9-62c0-4b32-928a-203f334da5d7

📥 Commits

Reviewing files that changed from the base of the PR and between d5e8e4f and d182c69.

📒 Files selected for processing (7)
  • packages/interfaces/src/distribution.cairo
  • packages/metagame/src/entry_fee/entry_fee_store.cairo
  • packages/metagame/src/prize/structs.cairo
  • packages/utilities/src/distribution/calculator.cairo
  • packages/utilities/src/distribution/payout.cairo
  • packages/utilities/src/distribution/structs.cairo
  • packages/utilities/src/distribution/tests/test_payout.cairo

pub const PAYOUT_TYPE_EXPONENTIAL: u8 = 2;
pub const PAYOUT_TYPE_UNIFORM: u8 = 3;
pub const PAYOUT_TYPE_CUSTOM: u8 = 4;
pub const PAYOUT_TYPE_GEOMETRIC: u8 = 5;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Exercise the new payout type in the packing tests.

test_fuzz_realistic_payout_types treats only values 0-4 as valid and uses % 5. It never exercises PAYOUT_TYPE_GEOMETRIC == 5. Include type 5 in the fuzz domain and add an API-level round-trip for Distribution::Geometric((10, 7)).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/metagame/src/prize/structs.cairo` at line 27, Update
test_fuzz_realistic_payout_types to treat PAYOUT_TYPE_GEOMETRIC as valid by
expanding the generated payout-type domain from 0–4 to 0–5. Add an API-level
packing/unpacking round-trip that covers Distribution::Geometric((10, 7)),
preserving the existing round-trip coverage for other distribution variants.

Comment thread packages/utilities/src/distribution/structs.cairo
Comment on lines +283 to +355
fn test_geometric_ratio_holds_between_adjacent_positions() {
let dist = Distribution::Geometric((10, 7));
let pool: u256 = 1_000_000_000_000_000_000;
let first = calculate_payout(dist, 1, 10, pool);
let second = calculate_payout(dist, 2, 10, pool);
assert!(first == 308720592627384808, "winner share, got {}", first);
assert!(second == 216104414839169366, "runner-up share, got {}", second);
// Each place takes 7/10 of the one above. Both sides are independently
// truncated from the exact ratio, so they can differ by a unit — comparing
// `second` against `first * 7 / 10` re-truncates an already-truncated
// value and is off by one here.
let expected = first * 7 / 10;
let drift = if second > expected {
second - expected
} else {
expected - second
};
assert!(drift <= 1, "adjacent ratio is b/a to within a unit, drifted {}", drift);
}

#[test]
fn test_geometric_winner_share_is_independent_of_field_size() {
let dist = Distribution::Geometric((10, 7));
let pool: u256 = 1_000_000_000_000_000_000;
// 1 - b/a = 30%, whether the field is 10 places or 39.
let small = calculate_payout(dist, 1, 10, pool);
let large = calculate_payout(dist, 1, 39, pool);
assert!(small > 308_000_000_000_000_000 && small < 309_000_000_000_000_000, "~30.9% at n=10");
assert!(large > 299_000_000_000_000_000 && large < 301_000_000_000_000_000, "~30.0% at n=39");
}

#[test]
fn test_geometric_conserves_the_pool() {
let dist = Distribution::Geometric((3, 2));
let pool: u256 = 1_000_000_000_000_000_000;
let n: u32 = 50;
let mut total: u256 = 0;
let mut p: u32 = 1;
while p <= n {
let amount = calculate_payout(dist, p, n, pool);
assert!(amount > 0, "position {} must be payable", p);
total += amount;
p += 1;
}
assert!(total <= pool, "never overpays");
assert!(pool - total < n.into(), "shortfall under one unit per position");
}

#[test]
fn test_max_geometric_payouts_matches_the_documented_reach() {
assert!(max_geometric_payouts(2) == 129, "50% decay reaches 129 places");
assert!(max_geometric_payouts(3) == 81, "2/3 decay reaches 81");
assert!(max_geometric_payouts(7) == 46, "5/7 decay reaches 46");
assert!(max_geometric_payouts(10) == 39, "7/10 decay reaches 39");
}

#[test]
#[should_panic(expected: "geometric ratio reaches at most 39 places")]
fn test_geometric_beyond_reach_is_refused_not_overflowed() {
let dist = Distribution::Geometric((10, 7));
calculate_payout(dist, 1, 40, 1_000_000_000_000_000_000);
}

#[test]
fn test_geometric_shape_guards() {
// b must be under a (otherwise the curve is flat or inverted), non-zero,
// and both must survive the single-u16 packed param slot.
assert!(supports_exact_payout(Distribution::Geometric((10, 7))), "valid ratio");
assert!(!supports_exact_payout(Distribution::Geometric((7, 10))), "inverted");
assert!(!supports_exact_payout(Distribution::Geometric((10, 10))), "flat");
assert!(!supports_exact_payout(Distribution::Geometric((10, 0))), "zero tail");
assert!(!supports_exact_payout(Distribution::Geometric((256, 7))), "exceeds the u8 slot");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the repository's existing Starknet Foundry fuzz-test conventions.
rg -n -C 3 --glob '*.cairo' '#\[.*fuzz|fuzz' packages

# Find configured coverage commands or tooling.
rg -n -C 3 --glob 'Scarb.toml' 'snforge|coverage' .

Repository: Provable-Games/game-components

Length of output: 50387


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## Target coverage references"
rg -n --glob 'Scarb.toml' 'coverage' --glob 'test_payout.cairo' .

echo
echo "## test_payout outline/size"
wc -l packages/utilities/src/distribution/tests/test_payout.cairo 2>/dev/null || true
ast-grep outline packages/utilities/src/distribution/tests/test_payout.cairo --view compact 2>/dev/null | sed -n '1,120p' || true

echo
echo "## Distribution module relevant symbols"
rg -n --glob '*.cairo' 'fn (calculate_payout|supports_exact_payout|max_geometric_payouts)|enum Distribution|struct Distribution' packages/utilities/src/distribution packages/utilities/src 2>/dev/null | sed -n '1,220p'

Repository: Provable-Games/game-components

Length of output: 198


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## target file"
wc -l packages/utilities/src/distribution/tests/test_payout.cairo 2>/dev/null || true
sed -n '1,120p' packages/utilities/src/distribution/tests/test_payout.cairo 2>/dev/null || true

echo
echo "## distribution sources"
fd -a -i 'distribution|payout' packages 2>/dev/null | sed -n '1,120p'
rg -n --glob '*.cairo' 'calculate_payout|supports_exact_payout|max_geometric_payouts|Geometric' packages/utilities 2>/dev/null | sed -n '1,220p' || true

echo
echo "## Scarb files"
fd -a 'Scarb.toml' . 2>/dev/null | sed -n '1,80p'
for f in $(fd 'Scarb.toml' . | sed -n '1,20p'); do
  echo "--- $f"
  sed -n '1,220p' "$f"
done

echo
echo "## coverage mentions"
rg -n -i --glob '!node_modules/**' 'coverage|snforge|fuzz' packages README* .github 2>/dev/null | sed -n '1,220p' || true

Repository: Provable-Games/game-components

Length of output: 47946


Add fuzz coverage for geometric payout boundaries.

The geometric tests use only fixed (10, 7)/(3, 2) ratios and field sizes. Add #[fuzzer] properties that exercise valid (a, b) pairs bounded by supports_exact_payout, max_geometric_payouts(a) boundaries, payout index edge cases, and pool sizes. Verify non-overpayment and the n-unit shortfall bound for each valid input, then ensure snforge test --coverage records the required coverage without regression.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/utilities/src/distribution/tests/test_payout.cairo` around lines 283
- 355, Add #[fuzzer] tests alongside test_geometric_conserves_the_pool covering
valid a,b ratios accepted by supports_exact_payout, field sizes at and around
max_geometric_payouts(a), payout indices including first, last, and boundary
values, and varied pool sizes. For every valid generated input, verify payouts
do not exceed the pool and total shortfall remains below n units, while
preserving existing fixed-case tests and confirming snforge test --coverage
passes without regression.

Source: Coding guidelines

Comment on lines +304 to +311
fn test_geometric_winner_share_is_independent_of_field_size() {
let dist = Distribution::Geometric((10, 7));
let pool: u256 = 1_000_000_000_000_000_000;
// 1 - b/a = 30%, whether the field is 10 places or 39.
let small = calculate_payout(dist, 1, 10, pool);
let large = calculate_payout(dist, 1, 39, pool);
assert!(small > 308_000_000_000_000_000 && small < 309_000_000_000_000_000, "~30.9% at n=10");
assert!(large > 299_000_000_000_000_000 && large < 301_000_000_000_000_000, "~30.0% at n=39");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Correct the finite-field geometric invariant.

The function name and Line 307 claim field-size independence. The assertions expect different winner shares for 10 and 39 places. Finite geometric normalization depends on n and approaches 1 - b/a as n grows. Rename the test and correct the comment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/utilities/src/distribution/tests/test_payout.cairo` around lines 304
- 311, Rename test_geometric_winner_share_is_independent_of_field_size to
reflect finite-field normalization depending on field size, and update the
comment above the calculations to state that the winner share approaches 1 - b/a
as n increases rather than claiming independence. Keep the existing assertions
and payout calculations unchanged.

starknetdev and others added 2 commits August 5, 2026 06:53
…e curve

`get_prize` eagerly called `get_custom_shares`, rebuilding the entire shares
array from packed storage as part of reconstructing the record. That runs on
every claim, and a claim settles exactly one position — so a 200-place Custom
prize paid O(count/15) storage reads plus 200 unpack steps, 200 times over,
to use one number.

The entry-fee store has never done this: `get_entry_fee` returns Custom with
an empty span and claims read a single share through `get_custom_share_at`.
This brings the prize side in line.

- `get_prize` reports the Custom *shape* with an empty span
- new `get_custom_share_at(prize_id, position)` — one storage read at any
  position, mirroring the entry-fee accessor
- view surfaces call `get_custom_shares` explicitly, as before

No prefix sums or precomputed totals are needed: shares are validated at
creation to number `distribution_count` and sum to exactly BASIS_POINTS, so
the denominator is the constant BASIS_POINTS and there is nothing to sum.
(Running totals would only be needed to pay the top n of a *longer* stored
curve, which is a separate capability.)

Measured in budokan, marginal cost of one distributed-prize claim:

    places      before       after    saving
        10   4,574,327   3,001,323      1.5x
        50  11,294,147   3,001,323      3.8x
       100  19,638,887   3,001,323      6.5x
       200  36,401,747   3,001,323     12.1x

Flat, and now marginally cheaper than Exponential (3,174,237), which computes
a power where this reads a slot.

snforge: 437 passed. The round-trip test now asserts the new contract — that
`get_prize` does not rebuild the curve, and that any position reads correctly
through the accessor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The variant that makes a very large field expressive. A single curve cannot:
anything steep enough to give first place a real share rounds its tail to
nothing, and anything flat enough to pay the tail gives first place nothing.
Over 10,000 places the best any single curve here can do for first place is
~0.06% (Exponential k=5).

`Tiered { head_ratio, head_count, head_share_bps }`: a geometric head over the
first `head_count` places takes `head_share_bps` of the pool; every remaining
place splits the rest evenly. The flagship configuration — top 39 on (10, 7)
taking 80% — over 10,000 places:

    1st     240000218290681776  (24.00% of a 1e18 pool)
    2nd     168000152803477243  (70% of 1st, exactly the decay)
    39th            311843831108
    40th..10000th    20078305391024 each (0.002%, all payable)
    residue 9,955 wei — under one unit per position

Both tiers are exact integer maths, and both are O(1): the head reuses
Geometric's closed form, the tail is one division. Measured: 3.50M l2_gas for
the winner, 2.31M for a tail place, flat across the field. The geometric reach
bound applies to `head_count`, not the field, which is exactly why the head
can stay steep on a field a hundred times larger.

Guards, refused at the boundary rather than misbehaving inside it:
- ratio rules are Geometric's (a > b > 0, a <= 255 for the packed param slot)
- 0 < head_share_bps < BASIS_POINTS — at either end one tier becomes an
  unclaimable zero, and the single-curve variants cover those shapes
- head_count within max_geometric_payouts(a); paid places strictly beyond the
  head

Storage: `PackedDistribution` and `PackedERC20Data` gain two u16 param slots
(head_count, head_share_bps), appended above the existing bit layouts so
previously packed values unpack unchanged. 88 and 216 bits respectively, both
comfortably inside felt252.

The weight helpers panic on Tiered: it is two pools, not one weight family,
and `calculate_payout` settles it before they are reached. The basis-point
calculator refuses it like Geometric — a tail place's true share is far under
a basis point by construction, so there is no honest bps form.

snforge: utilities 309 passed (the 2 failures are the pre-existing local
step-limit pair, green in CI), metagame 438 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

🧹 Nitpick comments (1)
packages/utilities/src/distribution/structs.cairo (1)

66-86: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Document the changed function contracts.

The changed functions explain implementation details, but they do not consistently document parameter types and constraints, return values, and usage examples where useful.

  • packages/utilities/src/distribution/structs.cairo#L66-L86: Document packing inputs, field ranges, unpacked output, and legacy-layout compatibility.
  • packages/metagame/src/entry_fee/entry_fee_store.cairo#L101-L124: Document packed ratio decoding, valid type tags, and the returned distribution/count contract.
  • packages/metagame/src/entry_fee/entry_fee_store.cairo#L243-L273: Document encoding constraints and the storage fields written for each distribution.
  • packages/metagame/src/prize/structs.cairo#L125-L230: Document ERC20 distribution serialization inputs, outputs, and Tiered field mapping.
  • packages/utilities/src/distribution/payout.cairo#L99-L393: Document each helper and public payout API parameter, constraints, return value, and a usage example where appropriate.

As per coding guidelines, Every function must include clear explanation of what it does and why, parameter descriptions with types and constraints, return value documentation, and example usage when appropriate.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/utilities/src/distribution/structs.cairo` around lines 66 - 86,
Document the changed function contracts across
packages/utilities/src/distribution/structs.cairo:66-86,
packages/metagame/src/entry_fee/entry_fee_store.cairo:101-124,
packages/metagame/src/entry_fee/entry_fee_store.cairo:243-273,
packages/metagame/src/prize/structs.cairo:125-230, and
packages/utilities/src/distribution/payout.cairo:99-393. Add Cairo documentation
for each affected function’s purpose, typed parameters and constraints, return
values, and appropriate usage examples; specifically cover PackedDistribution
field ranges and legacy-layout compatibility, packed-ratio tags and
distribution/count results, encoding and storage-field behavior, ERC20
serialization and Tiered mappings, and every payout helper and public API
contract.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/metagame/src/prize/prize_store.cairo`:
- Around line 78-90: Update packages/metagame/src/prize/prize_store.cairo:78-90
so PrizeComponentImpl::get_prize returns the fully reconstructed custom share
span through get_token_record, while claim paths use get_custom_share_at for the
indexed share. Update
packages/metagame/src/prize/tests/test_prize_store.cairo:401-411 to assert that
IPrize.get_prize exposes all stored custom shares.
- Around line 119-124: Update get_custom_share_at to validate that position is
within 1..=count before subtracting one, reject invalid positions without
performing a packed lookup, and restrict computed slot indices to supported
slots. In packages/metagame/src/prize/tests/test_prize_store.cairo:395-399, add
coverage for position zero, positions above count, empty prizes, and
slot-boundary positions.

In `@packages/metagame/src/prize/structs.cairo`:
- Around line 60-64: Update the storage-layout documentation adjacent to the
high-word construction in the prize struct encoding to describe the widened
216-bit representation with an 88-bit high word. Document param2 as occupying
bits 56–71 and param3 as occupying bits 72–87, keeping the existing arithmetic
layout unchanged.

---

Nitpick comments:
In `@packages/utilities/src/distribution/structs.cairo`:
- Around line 66-86: Document the changed function contracts across
packages/utilities/src/distribution/structs.cairo:66-86,
packages/metagame/src/entry_fee/entry_fee_store.cairo:101-124,
packages/metagame/src/entry_fee/entry_fee_store.cairo:243-273,
packages/metagame/src/prize/structs.cairo:125-230, and
packages/utilities/src/distribution/payout.cairo:99-393. Add Cairo documentation
for each affected function’s purpose, typed parameters and constraints, return
values, and appropriate usage examples; specifically cover PackedDistribution
field ranges and legacy-layout compatibility, packed-ratio tags and
distribution/count results, encoding and storage-field behavior, ERC20
serialization and Tiered mappings, and every payout helper and public API
contract.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3a9130de-1859-49cd-b651-af39a5c43914

📥 Commits

Reviewing files that changed from the base of the PR and between d182c69 and 5b7ae96.

📒 Files selected for processing (11)
  • packages/interfaces/src/distribution.cairo
  • packages/metagame/src/entry_fee/entry_fee_store.cairo
  • packages/metagame/src/prize/prize_component.cairo
  • packages/metagame/src/prize/prize_store.cairo
  • packages/metagame/src/prize/structs.cairo
  • packages/metagame/src/prize/tests/mocks/prize_mock.cairo
  • packages/metagame/src/prize/tests/test_prize_store.cairo
  • packages/utilities/src/distribution/calculator.cairo
  • packages/utilities/src/distribution/payout.cairo
  • packages/utilities/src/distribution/structs.cairo
  • packages/utilities/src/distribution/tests/test_payout.cairo
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/utilities/src/distribution/calculator.cairo

Comment on lines +78 to +90
// Return the shape with an empty span
// rather than rebuilding the curve.
// Loading it is O(count/15) storage
// reads on a path that runs on every
// claim, and a claim needs exactly one
// share — `get_custom_share_at`.
// View surfaces call
// `get_custom_shares` explicitly.
// Mirrors the entry-fee store, which
// has always done this.
Option::Some(
game_components_utilities::distribution::structs::Distribution::Custom(
shares.span(),
array![].span(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 8 \
  'get_prize\(|get_token_record\(|Distribution::Custom|_get_custom_share_at|get_custom_share_at|get_custom_shares' \
  packages --glob '*.cairo'

Repository: Provable-Games/game-components

Length of output: 50387


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== prize interface excerpt =="
sed -n '80,105p' packages/interfaces/src/prize.cairo

echo "== prize_store getter excerpt =="
sed -n '60,115p' packages/metagame/src/prize/prize_store.cairo
sed -n '190,225p' packages/metagame/src/prize/prize_store.cairo

echo "== prize_component/prize wrapper candidates =="
fd -a 'prize.*\.cairo|prize_impl.*\.cairo|prize_store.*\.cairo' packages/metagame/src 2>/dev/null || true
rg -n --glob '*.cairo' 'get_prize|to_token_record|PrizeStoreTrait|IPrize|IPrizeImpl|impl .*Prize' packages/metagame/src packages/interfaces/src

echo "== tests excerpt around custom get_prize =="
sed -n '380,415p' packages/metagame/src/prize/tests/test_prize_store.cairo

echo "== all get_prize references outside distributions =="
rg -n --glob '*.cairo' 'get_prize\(' packages --glob '!packages/utilities/src/distribution/**'

Repository: Provable-Games/game-components

Length of output: 23459


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== prize_component implementation excerpt =="
sed -n '212,275p' packages/metagame/src/prize/prize_component.cairo
sed -n '530,558p' packages/metagame/src/prize/prize_component.cairo

echo "== structs to_token_record and custom packing excerpts =="
sed -n '125,155p' packages/metagame/src/prize/structs.cairo
sed -n '198,220p' packages/metagame/src/prize/structs.cairo

echo "== external call references to IPrize/get_prize =="
rg -n --glob '*.cairo' 'IPrizeDispatcher|IPrizeDispatcherTrait|get_prize\(' packages --glob '!packages/metagame/src/prize/**'

echo "== broader references to custom distribution shape expectation =="
rg -n --glob '*.cairo' 'get_prize.*Custom|Custom.*get_prize|reread\.len|get_prize must not rebuild|get_token_record|get_custom_shares|get_custom_share_at' packages --glob '!packages/utilities/src/distribution/**'

Repository: Provable-Games/game-components

Length of output: 13998


Preserve the full custom share span on IPrize.get_prize.

PrizeComponentImpl::get_prize returns the result of get_token_record, which converts every stored Distribution::Custom to Custom(array![].span()). The public IPrize view therefore exposes an incomplete PrizeRecord. Keep get_prize returning the reconstructed custom shares, and have claim paths call the indexed share helper instead.

📍 Affects 2 files
  • packages/metagame/src/prize/prize_store.cairo#L78-L90 (this comment)
  • packages/metagame/src/prize/tests/test_prize_store.cairo#L401-L411
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/metagame/src/prize/prize_store.cairo` around lines 78 - 90, Update
packages/metagame/src/prize/prize_store.cairo:78-90 so
PrizeComponentImpl::get_prize returns the fully reconstructed custom share span
through get_token_record, while claim paths use get_custom_share_at for the
indexed share. Update
packages/metagame/src/prize/tests/test_prize_store.cairo:401-411 to assert that
IPrize.get_prize exposes all stored custom shares.

Comment on lines +119 to +124
fn get_custom_share_at(self: @T, prize_id: u64, position: u32) -> u16 {
let index: u32 = position - 1;
let slot_index: u8 = (index / CUSTOM_SHARES_PER_SLOT.into()).try_into().unwrap();
let index_in_slot: u8 = (index % CUSTOM_SHARES_PER_SLOT.into()).try_into().unwrap();
Store::get_custom_shares_packed(self, prize_id, slot_index).get_share(index_in_slot)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 8 \
  'get_custom_share_at|_get_custom_share_at|PrizeType::Distributed|set_prize_claimed|payout' \
  packages --glob '*.cairo'

Repository: Provable-Games/game-components

Length of output: 50387


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files:"
fd -a 'prize_store.cail|test_prize_store\.cairo|prize_component\.cairo|prize.*\.cairo' packages/metagame/src/prize packages/metagame/src 2>/dev/null | sed 's#^\./##'

echo
echo "prize_store outline:"
ast-grep outline packages/metagame/src/prize/prize_store.cairo --view expanded || true

echo
echo "Relevant store implementation:"
sed -n '1,190p' packages/metagame/src/prize/prize_store.cairo | cat -n

echo
echo "Relevant tests around custom share:"
sed -n '330,420p' packages/metagame/src/prize/tests/test_prize_store.cairo | cat -n

echo
echo "Claim/payout references in prize files only:"
rg -n -C 10 'set_prize_claimed|payout_prize|claim|Custom|get_custom_share_at|get_custom_shares|positions' packages/metagame/src/prize --glob '*.cairo'

Repository: Provable-Games/game-components

Length of output: 50387


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Packed share behavior probe:"
python3 - <<'PY'
# Mirrors the documented packed-share arithmetic in packages/metagame/src/prize/prize_store.cairo.
CUSTOM_SHARES_PER_SLOT = 15
positions = [0, 1, 15, 16, 17, 18, 384, 385]
count = 17
for position in positions:
    index = position - 1
    if index < 0:
        print(f"position {position}: underflows u32; no direct slot conversion")
        continue
    slot_index_u8 = (index // CUSTOM_SHARES_PER_SLOT)
    index_in_slot_u8 = (index % CUSTOM_SHARES_PER_SLOT)
    readable = 0 <= slot_index_u8 <= 255 and 0 <= index_in_slot_u8 <= 14
    status = "readable slot if slot exists" if readable else f"u8 conversion failure ({slot_index_u8=})"
    print(f"position {position}: zindex={index}, slot={slot_index_u8}, offset={index_in_slot_u8}, status={status}, in_stored_range={0 < position <= count}")
PY

echo
echo "Error constants and custom share tests:"
rg -n -C 4 'Prize:|Custom share|get_custom_share_at|position + 1|position - 1|CUSTOM_SHARE|SHARES_PER_SLOT' packages/metagame/src/prize --glob '*.cairo'

echo
echo "Read claim resolution in prize component around custom shares:"
rg -n -C 12 'get_custom_share_at|get_payout_position|set_prize_claimed|claim|Distributed|pout' packages/metagame/src/prize/prize_component.cairo | sed -n '1,260p'

Repository: Provable-Games/game-components

Length of output: 50386


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Determine claim-related code shape around get_custom_share_at callers"
rg -n 'get_custom_share_at|_get_custom_share_at|calculate_share|payout_prize|set_prize_claimed|get_claims|Claimable|claim_position|position' packages/metagame/src/prize/prize_component.cairo
echo
sed -n '1,340p' packages/metagame/src/prize/prize_component.cairo | cat -n
echo
sed -n '340,660p' packages/metagame/src/prize/prize_component.cairo | cat -n

echo
echo "Interface and claim type definitions in package interfaces"
fd -a 'prize.*\.cairo|claim.*\.cairo' packages packages/metagame 2>/dev/null | sed -n '1,200p'
rg -n 'iprize|IPRIZE|claim_prize|claims|ClaimType|position' packages --glob '*.cairo' | sed -n '1,260p'

Repository: Provable-Games/game-components

Length of output: 50386


Reject out-of-range custom shares before packed lookup.

position - 1 can underflow for position = 0, and values above count read beyond the configured distribution. Validate 1 <= position <= count before subtraction, keep the packed lookup limited to supported slot indices, and add coverages for invalid custom-share positions including empty prizes and slot boundaries.

📍 Affects 2 files
  • packages/metagame/src/prize/prize_store.cairo#L119-L124 (this comment)
  • packages/metagame/src/prize/tests/test_prize_store.cairo#L395-L399
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/metagame/src/prize/prize_store.cairo` around lines 119 - 124, Update
get_custom_share_at to validate that position is within 1..=count before
subtracting one, reject invalid positions without performing a packed lookup,
and restrict computed slot indices to supported slots. In
packages/metagame/src/prize/tests/test_prize_store.cairo:395-399, add coverage
for position zero, positions above count, empty prizes, and slot-boundary
positions.

Source: Coding guidelines

Comment on lines 60 to +64
let high: u128 = value.payout_type.into()
+ value.param.into() * 0x100_u128 // shift 8
+ value.count.into() * 0x1000000_u128; // shift 24
+ value.count.into() * 0x1000000_u128 // shift 24
+ value.param2.into() * 0x100000000000000_u128 // shift 56
+ value.param3.into() * 0x1000000000000000000_u128; // shift 72

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the widened high-word layout documentation.

The implementation now stores 216 bits with an 88-bit high word. The comments still describe 184 bits and a 56-bit high word. Document param2 at bits 56-71 and param3 at bits 72-87. Incorrect storage-layout documentation can cause a later decoder or migration to use invalid offsets.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/metagame/src/prize/structs.cairo` around lines 60 - 64, Update the
storage-layout documentation adjacent to the high-word construction in the prize
struct encoding to describe the widened 216-bit representation with an 88-bit
high word. Document param2 as occupying bits 56–71 and param3 as occupying bits
72–87, keeping the existing arithmetic layout unchanged.

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.

1 participant