From bbf1d396bc7c9ad390b42cc4528aeaa3891caf8f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 7 Mar 2026 09:53:47 -0800 Subject: [PATCH] fix: cap felt252_to_byte_array at 31 bytes to prevent 'bad append len' 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 --- .../utilities/src/renderer/metadata.cairo | 9 +--- packages/utilities/src/renderer/svg.cairo | 8 +-- packages/utilities/src/utils/encoding.cairo | 11 +++- .../src/utils/tests/test_encoding.cairo | 52 ++++++++++++++++++- 4 files changed, 65 insertions(+), 15 deletions(-) diff --git a/packages/utilities/src/renderer/metadata.cairo b/packages/utilities/src/renderer/metadata.cairo index 23e4565..501b1c0 100644 --- a/packages/utilities/src/renderer/metadata.cairo +++ b/packages/utilities/src/renderer/metadata.cairo @@ -6,7 +6,7 @@ use game_components_embeddable_game_standard::registry::interface::GameMetadata; use game_components_embeddable_game_standard::token::structs::TokenMetadata; use graffiti::json::JsonImpl; use starknet::{ContractAddress, get_block_timestamp}; -use crate::utils::encoding::{U256BytesUsedTraitImpl, bytes_base64_encode, felt252_to_byte_array}; +use crate::utils::encoding::{bytes_base64_encode, felt252_to_byte_array}; fn create_trait(name: ByteArray, value: ByteArray) -> ByteArray { JsonImpl::new().add("trait", name).add("value", value).build() @@ -104,12 +104,7 @@ pub fn create_custom_metadata( // Optional player name trait if !player_name.is_zero() { - let mut _player_name = Default::default(); - _player_name - .append_word( - player_name, U256BytesUsedTraitImpl::bytes_used(player_name.into()).into(), - ); - attributes.append(create_trait("Player Name", _player_name)); + attributes.append(create_trait("Player Name", felt252_to_byte_array(player_name))); } // Add dynamic game details as traits diff --git a/packages/utilities/src/renderer/svg.cairo b/packages/utilities/src/renderer/svg.cairo index 13d0115..4b5e1a7 100644 --- a/packages/utilities/src/renderer/svg.cairo +++ b/packages/utilities/src/renderer/svg.cairo @@ -8,7 +8,7 @@ use game_components_embeddable_game_standard::minigame::extensions::settings::st use game_components_embeddable_game_standard::registry::interface::GameMetadata; use game_components_embeddable_game_standard::token::structs::TokenMetadata; use starknet::get_block_timestamp; -use crate::utils::encoding::{U256BytesUsedTraitImpl, felt252_to_byte_array}; +use crate::utils::encoding::felt252_to_byte_array; fn create_text( text: ByteArray, @@ -250,11 +250,7 @@ pub fn create_default_svg( let mut _player_name: ByteArray = Default::default(); if player_name.is_non_zero() { - _player_name - .append_word( - player_name, U256BytesUsedTraitImpl::bytes_used(player_name.into()).into(), - ); - _player_name = uri_encode(_player_name); + _player_name = uri_encode(felt252_to_byte_array(player_name)); } else { _player_name = "---"; } diff --git a/packages/utilities/src/utils/encoding.cairo b/packages/utilities/src/utils/encoding.cairo index a284982..f3e19ac 100644 --- a/packages/utilities/src/utils/encoding.cairo +++ b/packages/utilities/src/utils/encoding.cairo @@ -186,7 +186,16 @@ pub impl U256BytesUsedTraitImpl of BytesUsedTrait { pub fn felt252_to_byte_array(value: felt252) -> ByteArray { let mut result: ByteArray = Default::default(); if value.is_non_zero() { - result.append_word(value, U256BytesUsedTraitImpl::bytes_used(value.into()).into()); + let len: u8 = U256BytesUsedTraitImpl::bytes_used(value.into()); + // ByteArray.append_word requires len <= 31. A felt252 encodes at most + // 31 bytes of short-string data, but bytes_used(u256) can return 32 for + // large values (high limb non-zero). Cap at 31 to prevent 'bad append len'. + let len: usize = if len > 31 { + 31 + } else { + len.into() + }; + result.append_word(value, len); } result } diff --git a/packages/utilities/src/utils/tests/test_encoding.cairo b/packages/utilities/src/utils/tests/test_encoding.cairo index a129809..f259ba3 100644 --- a/packages/utilities/src/utils/tests/test_encoding.cairo +++ b/packages/utilities/src/utils/tests/test_encoding.cairo @@ -5,7 +5,9 @@ // All functions are pure - no contract deployment needed. use core::num::traits::Bounded; -use crate::utils::encoding::{U256BytesUsedTraitImpl, bytes_base64_encode, u128_to_ascii_felt}; +use crate::utils::encoding::{ + U256BytesUsedTraitImpl, bytes_base64_encode, felt252_to_byte_array, u128_to_ascii_felt, +}; // ============================================================================== // BASE64 ENCODING TESTS @@ -647,3 +649,51 @@ fn test_u128_to_ascii_felt_panics_over_31_digits() { // u128 max = 340282366920938463463374607431768211455 (39 digits) u128_to_ascii_felt(340282366920938463463374607431768211455); } + +// ============================================================================== +// FELT252 TO BYTE ARRAY TESTS +// ============================================================================== + +#[test] +fn test_felt252_to_byte_array_zero() { + let result = felt252_to_byte_array(0); + assert!(result.len() == 0, "Zero should produce empty ByteArray"); +} + +#[test] +fn test_felt252_to_byte_array_short_string() { + let result = felt252_to_byte_array('hello'); + assert!(result == "hello", "Should convert short string correctly"); +} + +#[test] +fn test_felt252_to_byte_array_max_short_string() { + // 31-byte short string (max that fits in a single felt252 word) + let result = felt252_to_byte_array('1234567890123456789012345678901'); + assert!(result.len() == 31, "31-byte string should produce 31-byte ByteArray"); +} + +#[test] +fn test_felt252_to_byte_array_large_value_no_panic() { + // A felt252 value large enough that U256BytesUsedTraitImpl::bytes_used + // returns 32, which previously caused 'bad append len' panic. + // The Starknet prime P ≈ 2^251, so any felt252 where the u256 + // representation has a non-zero high limb AND bytes_used(high) >= 16 + // would return 32 bytes. We use P-1 (max valid felt252). + let large: felt252 = (-1); // P - 1, the largest valid felt252 + let result = felt252_to_byte_array(large); + // Should not panic — length is capped at 31 + assert!(result.len() == 31, "Large felt252 should produce 31-byte ByteArray"); +} + +#[test] +#[fuzzer(runs: 256)] +fn test_fuzz_felt252_to_byte_array_no_panic(value: felt252) { + // Should never panic regardless of input + let result = felt252_to_byte_array(value); + if value == 0 { + assert!(result.len() == 0, "Zero should produce empty ByteArray"); + } else { + assert!(result.len() > 0 && result.len() <= 31, "Non-zero should produce 1-31 bytes"); + } +}