Skip to content

Generated mapping getters encode wide keys with uint256 semantics #20

Description

@moscowchill

Summary

The legacy code generator encodes every generated public mapping getter key with uint256 memory-store semantics before hashing the storage slot. On a 64-byte Hyperion target, this changes the canonical key encoding for wider or differently aligned value types.

The affected call is in ExpressionCompiler::appendStateVariableAccessor(). It calls CompilerUtils::storeInMemory(0), whose implementation always uses TypeProvider::uint256().

This source shape is present at upstream main commit f570687a and the current QRL 64-byte feature head 6f862206.

Minimal reproduction

contract C {
    mapping(bytes32 => uint8) public byBytes32;
    mapping(address => uint8) public byAddress;
    mapping(uint512 => uint8) public byUint512;
    mapping(int256 => uint8) public byInt256;

    function setBytes32(bytes32 key, uint8 value) public { byBytes32[key] = value; }
    function setAddress(address key, uint8 value) public { byAddress[key] = value; }
    function setUint512(uint512 key, uint8 value) public { byUint512[key] = value; }
    function setInt256(int256 key, uint8 value) public { byInt256[key] = value; }
}

Representative keys that expose the mismatch:

  • address: a full 64-byte value with nonzero upper and lower 32-byte halves
  • bytes32: a nonzero 32-byte value, whose ABI alignment differs from uint256
  • uint512: any value above 2^256 - 1
  • int256: -1, which requires signed canonicalization

With legacy codegen, the generated getters return zero for entries written under these exact keys. Observed coercions include a left-aligned bytes32 becoming zero, a 64-byte address losing its upper 32 bytes, uint512(2^256 + 42) becoming 42, and int256(-1) losing canonical 512-bit sign extension. The via-IR accessor path does not use this legacy helper.

Expected behavior

A generated getter must hash the exact canonical encoding for its declared mapping key type. It must retrieve the same slot used by an explicit mapping read for every supported key type.

Root cause

CompilerUtils::storeInMemory(unsigned) calls:

prepareMemoryStore(*TypeProvider::uint256(), true)

The generated accessor therefore applies uint256 cleanup and alignment regardless of the declared key type.

Suggested fix

Add a typed memory-store overload and pass *paramTypes[i] from appendStateVariableAccessor(). Keep the existing untyped helper for callers that intentionally store uint256.

A semantic regression should cover bytes32, a full-width address, uint512 > 2^256, and negative int256, with both legacy and via-IR compilation plus default and optimized execution. A local typed-store fix produced the expected distinct results in all four modes: the exact keys returned 7, 5, 9, and 10, while zero, low-only 0xab, and 42 comparison keys returned zero.

Impact

This is a correctness issue for generated public getters on 64-byte targets. Contract state can be written correctly while the generated getter hashes a different key encoding and returns the wrong slot or zero.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions