Skip to content

Render a raw signed integer as signed - #12

Open
moodysalem wants to merge 1 commit into
llbartekll:mainfrom
EkuboProtocol:fix-raw-signed-integers
Open

Render a raw signed integer as signed#12
moodysalem wants to merge 1 commit into
llbartekll:mainfrom
EkuboProtocol:fix-raw-signed-integers

Conversation

@moodysalem

Copy link
Copy Markdown

format_raw shares one match arm between ArgumentValue::Uint and
ArgumentValue::Int and puts both through BigUint::from_bytes_be, so every
negative value prints as its two's-complement word.

ArgumentValue::Uint(bytes) | ArgumentValue::Int(bytes) => {
    let n = BigUint::from_bytes_be(bytes);
    n.to_string()
}

A Uniswap-style tick of -140 renders as

115792089237316195423570985008687907853269984665640564039457584007913129639796

which is the part worth emphasising: it is not a number a reader can recognise
as wrong. It is a plausible-looking enormous integer, on the screen whose entire
job is telling someone what they are about to sign.

Why descriptors cannot work around it

ERC-7730 defines raw as "the natural representation of the underlying
structured data type", and for intN that is signed. The v2 schema has no
signed-integer format at all, so raw is the only thing a descriptor author
can reach for. A descriptor that wants a signed display has nowhere else to go.

In practice authors have been reaching for number, which renders correctly
here because format_number already calls int_to_bigint — but number is
not in the v2 schema, so those descriptors fail schema validation. Fixing the
engine lets them be spec-conformant and correct at the same time.

The change

Split the arm and point Int at the existing int_to_bigint, which already
does the two's-complement conversion for format_number:

ArgumentValue::Uint(bytes) => BigUint::from_bytes_be(bytes).to_string(),
ArgumentValue::Int(bytes) => int_to_bigint(bytes).to_string(),

The Uint arm is unchanged. The test pins both sides of the boundary — the
same 32-byte word read as Int and as Uint, plus zero, -1, and the sign-bit
edge — so the fix cannot later be simplified back into a shared arm.

format_raw_with_separator inherits it through its per-item format_raw call.
The EIP-712 renderer's Raw path is unaffected: it formats JSON values, where
a negative number already arrives as "-140" rather than as a word.

Testing

cargo test --workspace passes: 139 unit tests plus every integration suite,
with no existing expectation changed. Nothing in the corpus was relying on the
unsigned reading.

Found while tracking down why a wallet's approval screen showed a 78-digit
number for a liquidity position's lower tick.

ERC-7730 calls `raw` "the natural representation of the underlying structured
data type", and for `intN` that is a signed number. `format_raw` shared one
match arm between `Uint` and `Int` and put both through
`BigUint::from_bytes_be`, so every negative value was printed as its
two's-complement word.

The failure mode is worse than a wrong-looking number. A Uniswap-style tick of
-140 rendered as 115792089237316195423570985008687907853269984665640564039457584007913129639796,
which is not obviously wrong to a reader — it is a plausible-looking enormous
integer on the screen whose entire job is telling someone what they are about
to sign. Descriptor authors have no way to route around it either: the v2
schema has no signed-integer format, so `raw` is the only way to ask for one.

`int_to_bigint` already existed for `format_number` and does the two's
complement correctly; this points the `Int` arm at it. The `Uint` arm is
untouched, and the test pins both sides of the boundary so the fix cannot
later be "simplified" back into a shared arm.

The EIP-712 renderer's `Raw` path is not affected: it formats JSON values,
where a negative number already arrives as "-140" rather than as a word.
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.

1 participant