fix(StdStorage): handle signed fields narrower than 256 bits - #904
Open
pucedoteth wants to merge 1 commit into
Open
fix(StdStorage): handle signed fields narrower than 256 bits#904pucedoteth wants to merge 1 commit into
pucedoteth wants to merge 1 commit into
Conversation
A getter whose return type is a signed integer narrower than 256 bits
ABI-encodes its value sign-extended, while storage holds only the
field's own bits. `int64(-5)` comes back from the call as
0xff..fffb but sits in the slot as 0x..fffffffffffffffb, so the two
never compare equal and `find` walks past the slot that holds it:
stdStorage find(StdStorage): Slot(s) not found.
This affects every negative value in an `int8`..`int248` field,
whether it shares a slot or sits in one alone, with or without
`enable_packed_slots`. Full-width `int256` is unaffected, because
there is nothing to extend. Unsigned fields are unaffected, because
their high bits are already zero.
Three changes, one cause:
- `find` compares the call result truncated to the field's width. For
a full-width field the mask is all ones, so this is a no-op.
- `read_int` sign-extends the extracted field back to `int256` from
the field's width, so a narrow negative value reads as itself rather
than as its zero-extension (-5 rather than 18446744073709551611).
- `checked_write` narrows a sign-extended negative back to the field
before the fit check. Previously `checked_write_int(-42)` on a packed
field failed the packed-slot bound with a misleading message about
not fitting a value "greater than 18446744073709551616". The getter
still returns the sign-extended form, so the write verification is
unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pucedoteth
requested review from
0xrusowsky,
DaniPopes,
grandizzy,
mattsse and
onbjerg
as code owners
August 23, 2026 16:18
DaniPopes
approved these changes
Aug 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A getter whose return type is a signed integer narrower than 256 bits ABI-encodes its value sign-extended, while storage holds only the field's own bits.
int64(-5)comes back from the call asbut sits in the slot as
0x…fffffffffffffffb.findcompares the two directly:so they never match and the loop walks past the slot that actually holds the value:
Scope
Any negative value in an
int8…int248field, whether it shares a slot or sits in one alone, and with or withoutenable_packed_slots.int256is unaffected — there is nothing to extend. The existingtest_StorageReadInt(type(int256).min) keeps passing.Changes
Three changes, one cause:
findcompares the call result truncated to the field's width. For a full-width field the mask is all ones, so this is a no-op.read_intsign-extends the extracted field back toint256from the field's width. Without this,findsucceeds but the value reads as its zero-extension —18446744073709551611instead of-5.checked_writenarrows a sign-extended negative back to the field before the fit check. Previouslychecked_write_int(-42)on a packed field tripped the packed-slot bound with a misleading message about not fitting a value "greater than 18446744073709551616". The getter still returns the sign-extended form, so the write verification below is unchanged.read_intno longer routes through_read, since it needs the field width;_readis still used byread_bytes32/read_address/read_uint, which must not sign-extend.read_booldelegates toread_intand is unaffected (0 and 1 extend to themselves).Test plan
Four tests added next to the existing
test_StorageReadInt, plus narrow signed fields onStorageTest(int64 tJpacked withuint64 tK, andint8 tSolopushed into its own slot by a followinguint256). All state vars are appended, and nothing in the suite hardcodes a slot index — every test discovers slots viafind().Verified by reverting only
src/StdStorage.soland keeping the tests:test_StorageReadIntPackedNegativeSlot(s) not found.test_StorageReadIntSoloNegativeSlot(s) not found.test_StorageWriteIntPackedNegativeSlot(s) not found.test_StorageReadIntPackedPositiveSiblingforge testis green: 210 passed, 0 failed (206 before, plus these 4), including the whole existing StdStorage suite — packed-slot fuzzing, struct depth, short bytes/string, and theenable_packed_slotspaths.forge fmt --checkis clean.🤖 Written with Claude Code. All output above is from a local
forge testrun.