ABI.TypeEncoder.encode_int/2 in lib/abi/type_encoder.ex mixes up bytes and bits in its overflow guard:
defp encode_int(int, desired_size_bits) when rem(desired_size_bits, 8) == 0 and is_integer(int) do
desired_size_bytes = ceil(desired_size_bits / 8)
# ...
significant_bytes =
if int >= 0 do
maybe_encode_unsigned(abs(int))
else
actual_bit_size = (-1 * int) |> :binary.encode_unsigned() |> bit_size()
maybe_encode_unsigned(2 ** actual_bit_size + int)
end
if byte_size(significant_bytes) > desired_size_bytes - 1 do
raise("Data overflow encoding int, data `#{int}` cannot fit in #{(desired_size_bytes - 1) * 8} bits")
end
# ...
end
The guard byte_size(significant_bytes) > desired_size_bytes - 1 compares a byte count against desired_size_bytes - 1, which evaluates to 0 for int8 (desired_size_bytes = 1). Since :binary.encode_unsigned(0) returns <<0>> (one byte), even 0 trips the guard — so for int8 the encoder rejects every value in the valid signed range -128..127, including 0. The same scaling error makes int16 reject any value whose two's-complement representation needs more than 1 byte (i.e. roughly anything outside -255..255), and so on for larger int<N> widths.
Repro:
iex> ABI.encode("f(int8)", [0])
** (RuntimeError) Data overflow encoding int, data `0` cannot fit in 0 bits
iex> ABI.encode("f(int8)", [42])
** (RuntimeError) Data overflow encoding int, data `42` cannot fit in 0 bits
iex> ABI.encode("f(int8)", [-1])
** (RuntimeError) Data overflow encoding int, data `-1` cannot fit in 0 bits
iex> ABI.encode("f(int16)", [1000])
** (RuntimeError) Data overflow encoding int, data `1000` cannot fit in 8 bits
The error message itself (cannot fit in 0 bits, cannot fit in 8 bits) is the giveaway — the check was meant to compare against desired_size_bits, not (desired_size_bytes - 1) * 8.
The existing test (test/abi/type_encoder_test.exs — "int overflow raises data overflow") passed only because it asserted the encoder raises on overflow — which it did, just for the wrong reason (it raised on essentially every value).
Suggested fix
Range-check the integer against the actual signed int<N> range up front, before any byte-encoding:
defp encode_int(int, desired_size_bits) when rem(desired_size_bits, 8) == 0 and is_integer(int) do
desired_size_bytes = ceil(desired_size_bits / 8)
max = Bitwise.bsl(1, desired_size_bits - 1)
if int >= max or int < -max do
raise(
"Data overflow encoding int, data `#{int}` cannot fit in #{desired_size_bits}-bit signed range (-#{max}..#{max - 1})"
)
end
# ... existing sign-byte / two's-complement encoding unchanged ...
end
This accepts the full signed range -2^(N-1)..2^(N-1)-1 for every int<N> and produces a correct, informative overflow message at the boundary.
Surfaced by a decode(encode(x)) == x property suite (stream_data) that walks every type in ABI.FunctionSelector.@type type/0. Happy to send a PR with the guard fix and tightened boundary tests (in-range values encode; 128 / -129 raise for int8; etc.) if this direction works.
ABI.TypeEncoder.encode_int/2inlib/abi/type_encoder.exmixes up bytes and bits in its overflow guard:The guard
byte_size(significant_bytes) > desired_size_bytes - 1compares a byte count againstdesired_size_bytes - 1, which evaluates to0forint8(desired_size_bytes = 1). Since:binary.encode_unsigned(0)returns<<0>>(one byte), even0trips the guard — so forint8the encoder rejects every value in the valid signed range-128..127, including0. The same scaling error makesint16reject any value whose two's-complement representation needs more than 1 byte (i.e. roughly anything outside-255..255), and so on for largerint<N>widths.Repro:
The error message itself (
cannot fit in 0 bits,cannot fit in 8 bits) is the giveaway — the check was meant to compare againstdesired_size_bits, not(desired_size_bytes - 1) * 8.The existing test (
test/abi/type_encoder_test.exs—"int overflow raises data overflow") passed only because it asserted the encoder raises on overflow — which it did, just for the wrong reason (it raised on essentially every value).Suggested fix
Range-check the integer against the actual signed
int<N>range up front, before any byte-encoding:This accepts the full signed range
-2^(N-1)..2^(N-1)-1for everyint<N>and produces a correct, informative overflow message at the boundary.Surfaced by a
decode(encode(x)) == xproperty suite (stream_data) that walks every type inABI.FunctionSelector.@type type/0. Happy to send a PR with the guard fix and tightened boundary tests (in-range values encode;128/-129raise forint8; etc.) if this direction works.