Skip to content

fix: cap felt252_to_byte_array at 31 bytes to prevent 'bad append len'#74

Merged
starknetdev merged 1 commit intonextfrom
fix/felt252-bytes-used-overflow
Mar 7, 2026
Merged

fix: cap felt252_to_byte_array at 31 bytes to prevent 'bad append len'#74
starknetdev merged 1 commit intonextfrom
fix/felt252-bytes-used-overflow

Conversation

@starknetdev
Copy link
Member

Summary

  • ByteArray.append_word(word, len) requires len <= 31, but U256BytesUsedTraitImpl::bytes_used() returns 32 for felt252 values >= 2^248 (where the u256 high limb is non-zero). This causes a bad append len panic during token_uri SVG/metadata rendering.
  • Cap the length at 31 in felt252_to_byte_array since a felt252 can encode at most 31 bytes of short-string data.
  • Replace inline append_word + bytes_used calls in metadata.cairo and svg.cairo with felt252_to_byte_array to centralize the fix.

Root Cause

// encoding.cairo:189 (before fix)
result.append_word(value, U256BytesUsedTraitImpl::bytes_used(value.into()).into());
//                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
//                        Returns 32 for felt252 values with non-zero u256 high limb
//                        → panics with 'bad append len'

U256BytesUsedTraitImpl::bytes_used() computes bytes_used(high) + 16, which can be up to 32. But append_word only accepts len <= 31.

Changes

  • packages/utilities/src/utils/encoding.cairo — cap at 31 in felt252_to_byte_array
  • packages/utilities/src/renderer/metadata.cairo — use felt252_to_byte_array for player_name
  • packages/utilities/src/renderer/svg.cairo — use felt252_to_byte_array for player_name
  • packages/utilities/src/utils/tests/test_encoding.cairo — 5 new tests including 256-run fuzz test

Test plan

  • snforge test -p game_components_utilities — 222 tests passing (5 new)
  • Fuzz test confirms felt252_to_byte_array never panics for any felt252 value
  • scarb fmt --check — clean

🤖 Generated with Claude Code

ByteArray.append_word requires len <= 31, but U256BytesUsedTraitImpl::bytes_used
can return 32 for felt252 values where the u256 high limb is non-zero (any
felt252 >= 2^248). This causes a 'bad append len' panic in token_uri rendering
for tokens with large felt252 game detail values or player names.

Cap the length at 31 in felt252_to_byte_array and replace inline
append_word + bytes_used calls in metadata.cairo and svg.cairo with
felt252_to_byte_array to centralize the fix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Mar 7, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

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

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8cac7028-aca6-4ec8-8f16-cb14427b7e79

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

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/felt252-bytes-used-overflow

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

❤️ Share

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

@gemini-code-assist
Copy link

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses a critical runtime panic occurring during token_uri SVG/metadata rendering, specifically when converting large felt252 values to ByteArray. The issue stemmed from U256BytesUsedTraitImpl::bytes_used() returning 32 bytes for certain felt252 values, which exceeds the ByteArray.append_word function's maximum allowed length of 31 bytes. The solution centralizes the felt252 to ByteArray conversion logic, ensuring the byte length is correctly capped at 31, and integrates this fix into the affected rendering components, thereby preventing future panics and improving system stability.

Highlights

  • felt252_to_byte_array length capping: Modified felt252_to_byte_array to cap the byte length at 31 when converting felt252 values to ByteArray, preventing bad append len panics for large values.
  • Centralized felt252 to ByteArray conversion: Replaced direct append_word and bytes_used calls with the now robust felt252_to_byte_array function in metadata.cairo and svg.cairo for player_name handling.
  • Enhanced test coverage: Added five new tests for felt252_to_byte_array, including a 256-run fuzz test, to ensure its reliability across all possible felt252 values.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • packages/utilities/src/renderer/metadata.cairo
    • Removed U256BytesUsedTraitImpl import.
    • Updated create_custom_metadata to use felt252_to_byte_array for player name.
  • packages/utilities/src/renderer/svg.cairo
    • Removed U256BytesUsedTraitImpl import.
    • Updated create_default_svg to use felt252_to_byte_array for player name.
  • packages/utilities/src/utils/encoding.cairo
    • Modified felt252_to_byte_array to cap the length at 31 bytes before appending the word, preventing bad append len panics.
  • packages/utilities/src/utils/tests/test_encoding.cairo
    • Imported felt252_to_byte_array.
    • Added new tests for felt252_to_byte_array, covering zero, short strings, max short strings, large values, and a fuzz test.
Activity
  • All existing tests (snforge test -p game_components_utilities) passed, with 5 new tests introduced.
  • A fuzz test confirmed that felt252_to_byte_array does not panic for any felt252 input value.
  • Code formatting (scarb fmt --check) was verified to be clean.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@claude
Copy link

claude bot commented Mar 7, 2026

Claude finished @starknetdev's task —— View job


Claude Review - General Engineering Review

run=22804131459 attempt=1 sha=bbf1d396bc7c9ad390b42cc4528aeaa3891caf8f scope=general

Todo List

  • Check git diff for changes outside packages/
  • Analyze CI/CD configuration changes
  • Review documentation updates
  • Check tooling and script modifications
  • Provide findings summary

Review Results

After analyzing the PR diff with the command:

git diff 3322d946804c02ea0b56c821ad81b80388c24fdb...bbf1d396bc7c9ad390b42cc4528aeaa3891caf8f -- . ':(exclude)packages/**'

No issues found. This PR contains only changes within the packages/ directory (Cairo contract code), which falls outside my review scope per the established boundary. All modified files are Cairo contracts:

  • packages/utilities/src/renderer/metadata.cairo
  • packages/utilities/src/renderer/svg.cairo
  • packages/utilities/src/utils/encoding.cairo
  • packages/utilities/src/utils/tests/test_encoding.cairo

No changes were made to:

  • CI/CD workflows (.github/workflows/)
  • Documentation files (README.md, CLAUDE.md, etc.)
  • Build scripts or tooling configuration
  • Repository configuration files

The Cairo-specific code review is handled by the dedicated Cairo reviewer.

Summary: 0 CRITICAL, 0 HIGH, 0 MEDIUM, 0 LOW, 0 INFO

@claude
Copy link

claude bot commented Mar 7, 2026

Claude finished @starknetdev's task —— View job


Claude Review - Cairo/Starknet Contract Review

run=22804131459 attempt=1 sha=bbf1d396bc7c9ad390b42cc4528aeaa3891caf8f scope=packages

Review Findings

This PR addresses a critical runtime panic in SVG/metadata rendering caused by ByteArray.append_word length validation. The fix is technically sound and well-implemented.

✅ SECURITY REVIEW: No Issues Found

  • felt252/u256 conversion safety: The capping logic is mathematically correct. felt252 values can represent at most 31 bytes of short-string data due to the Starknet prime (~2^251)
  • ByteArray constraints: Properly respects append_word(word, len) requirement that len <= 31
  • No overflow/underflow risks: Length conversion len.into() is safe since len is capped at 31

✅ CAIRO IDIOMS: Clean Implementation

  • Proper use of is_non_zero() for felt252 zero checks
  • Consistent with existing codebase patterns
  • Clean removal of unused imports (U256BytesUsedTraitImpl)
  • Function remains pure/deterministic

✅ TESTING: Comprehensive Coverage

Strong test suite with 5 new tests:

  • Zero value handling: test_felt252_to_byte_array_zero()
  • Short string conversion: test_felt252_to_byte_array_short_string()
  • Boundary condition: test_felt252_to_byte_array_max_short_string() (31 bytes)
  • Regression test: test_felt252_to_byte_array_large_value_no_panic() using felt252(-1) (P-1)
  • Comprehensive fuzz test: test_fuzz_felt252_to_byte_array_no_panic() with 256 runs

The fuzz test is particularly valuable—it confirms the function never panics for any felt252 input.

✅ GAS OPTIMIZATION: Improvement

  • Centralized logic: Eliminates duplicate inline append_word + bytes_used patterns
  • Reduced complexity: Single function call vs. inline computation in renderer modules
  • Better caching: Consistent function reuse

✅ ARCHITECTURE: Well-Designed

  • Centralized fix: The encoding module is the correct place for this logic
  • Backwards compatible: No breaking API changes
  • Clear separation: Pure encoding logic separated from rendering concerns

Root Cause Analysis Verification ✓

The PR description accurately identifies the issue:

// Before (encoding.cairo:189) - PROBLEMATIC
result.append_word(value, U256BytesUsedTraitImpl::bytes_used(value.into()).into());
//                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 
//                        Can return 32 for felt252 >= 2^248 → panic: 'bad append len'

// After (encoding.cairo:189-198) - FIXED
let len: u8 = U256BytesUsedTraitImpl::bytes_used(value.into());
let len: usize = if len > 31 { 31 } else { len.into() };  // Cap at 31
result.append_word(value, len);

Mathematical validation: For felt252 values where u256.high ≠ 0 and bytes_used(high) ≥ 16, the total becomes 16 + bytes_used(high) ≥ 32, which exceeds ByteArray's 31-byte limit.

Code Quality Assessment ✓

packages/utilities/src/utils/encoding.cairo:186-201 - The core fix is excellent:

  • Clear, well-commented explanation of the constraint
  • Mathematically sound capping logic
  • Preserves all valid short-string data (felt252 truly cannot encode more than 31 bytes)

packages/utilities/src/renderer/metadata.cairo:107 & packages/utilities/src/renderer/svg.cairo:253 - Clean refactoring:

  • Eliminates error-prone inline logic
  • Improves maintainability through centralization

Summary: 0 CRITICAL, 0 HIGH, 0 MEDIUM, 0 LOW, 0 INFO

APPROVED ✅ This is a high-quality bug fix that:

  • Resolves a critical runtime panic with mathematical precision
  • Includes comprehensive test coverage including fuzz testing
  • Improves code maintainability through centralization
  • Follows Cairo best practices throughout

The fix is production-ready and should prevent this class of error across the entire codebase.


@github-actions
Copy link

github-actions bot commented Mar 7, 2026

Codex Review - General Engineering Review

No issues found.
Summary: 0 CRITICAL, 0 HIGH, 0 MEDIUM, 0 LOW, 0 INFO

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request correctly addresses a panic caused by ByteArray.append_word when handling large felt252 values. The fix, which involves capping the length at 31 bytes within a centralized felt252_to_byte_array function, is well-implemented. The refactoring in metadata.cairo and svg.cairo to use this new function is clean. The addition of comprehensive tests, including a fuzz test, is excellent and ensures the robustness of the fix. I have one minor suggestion to improve conciseness in the encoding.cairo file.

Comment on lines +193 to +198
let len: usize = if len > 31 {
31
} else {
len.into()
};
result.append_word(value, len);

Choose a reason for hiding this comment

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

medium

For conciseness and to avoid variable shadowing, you can combine the length capping and the function call into a single expression. This makes the code slightly more direct and idiomatic.

        result.append_word(value, if len > 31 { 31 } else { len.into() });

@github-actions
Copy link

github-actions bot commented Mar 7, 2026

Codex Review - Cairo/Starknet Contract Review

No issues found.
Summary: 0 CRITICAL, 0 HIGH, 0 MEDIUM, 0 LOW, 0 INFO

@codecov
Copy link

codecov bot commented Mar 7, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@starknetdev starknetdev merged commit 372b00d into next Mar 7, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants