Zellic finding 3.9 (Medium, availability). ArrayType::isExplicitlyConvertibleTo accepts sbytesN(sb) (shielded dynamic byte array -> fixed shielded-bytes), but neither codegen backend had a matching branch: CompilerUtils::convertType (legacy) and YulUtilFunctions::conversionFunction (via-IR) only handle a FixedBytes target, so a ShieldedFixedBytes target fell through to a solAssert and aborted with an InternalCompilerError. Any contract using the documented sbytesN(sb) pattern was uncompilable.
Fix (Zellic's two options, split by source location)
- Memory / calldata source (a plain byte read; the finding's PoC is memory): route through the existing
bytesToFixedBytesConversionFunction, mirroring bytes -> bytesN. Guard broadened in both backends and both source kinds (Array and ArraySlice).
- Storage source: rejected at the type checker with a user-facing error instead of crashing. A correct storage conversion needs a shielded-layout-aware read (
cload against the array's data area); the public conversion machinery bakes in the public bytes layout and returns 0 for a shielded storage source, so routing it there would replace the ICE with silent data corruption. Rejecting is Zellic's sanctioned alternative and steers to the working memory form:
Conversion of a shielded byte array in storage to a fixed shielded-bytes type is not supported; copy it to memory first (e.g. sbytesN(sbytes memory)).
Why not full storage support here
The storage path is a genuine codegen feature (shielded data-area read in both backends, full semantic verification), not routing. This resolves the actual finding (the crash) and delivers the common case correctly; direct-from-storage conversion can be a follow-up.
Tests
shielded_sbytes_memory_to_fixed.sol (new semantic): memory + calldata-slice sources, verified against the public bytesN baseline across all four pipeline configs.
sbytes_dynamic_to_fixed.sol, sbytes_dynamic_conversions.sol: were syntax-only and asserted the storage cast type-checks (never running codegen where it crashed); updated to cover memory-allowed + storage-rejected.
- Full syntax suite green; semanticTests/types + conversions regression-swept clean.
Zellic finding 3.9 (Medium, availability).
ArrayType::isExplicitlyConvertibleToacceptssbytesN(sb)(shielded dynamic byte array -> fixed shielded-bytes), but neither codegen backend had a matching branch:CompilerUtils::convertType(legacy) andYulUtilFunctions::conversionFunction(via-IR) only handle aFixedBytestarget, so aShieldedFixedBytestarget fell through to asolAssertand aborted with an InternalCompilerError. Any contract using the documentedsbytesN(sb)pattern was uncompilable.Fix (Zellic's two options, split by source location)
bytesToFixedBytesConversionFunction, mirroringbytes -> bytesN. Guard broadened in both backends and both source kinds (ArrayandArraySlice).cloadagainst the array's data area); the public conversion machinery bakes in the public bytes layout and returns0for a shielded storage source, so routing it there would replace the ICE with silent data corruption. Rejecting is Zellic's sanctioned alternative and steers to the working memory form:Why not full storage support here
The storage path is a genuine codegen feature (shielded data-area read in both backends, full semantic verification), not routing. This resolves the actual finding (the crash) and delivers the common case correctly; direct-from-storage conversion can be a follow-up.
Tests
shielded_sbytes_memory_to_fixed.sol(new semantic): memory + calldata-slice sources, verified against the publicbytesNbaseline across all four pipeline configs.sbytes_dynamic_to_fixed.sol,sbytes_dynamic_conversions.sol: were syntax-only and asserted the storage cast type-checks (never running codegen where it crashed); updated to cover memory-allowed + storage-rejected.