EEVM.ABI (eevm v0.1.0)

Copy Markdown View Source

Encodes and decodes Solidity ABI data: calldata for a function call, return data, and event logs, as the Solidity ABI specification defines them.

iex> calldata = EEVM.ABI.encode_call("transfer(address,uint256)", [0xB0B, 250])
iex> byte_size(calldata)
68
iex> EEVM.ABI.decode(["bool"], <<1::256>>)
{:ok, [true]}

Types are Solidity type strings, such as "uint256", "address[]", or "(uint256,bytes)[2]" for a fixed-size array of tuples. Signatures may carry parameter names and spaces, as in Solidity source, and uint and int mean uint256 and int256; selectors and event topics are always computed from the canonical form, so "transfer(address to, uint amount)" encodes the same calldata as "transfer(address,uint256)".

Values

ABI typeElixir value
uint<N>, int<N>integer
addressinteger, as eevm represents addresses; encoding also takes anything EEVM.address/1 takes
booltrue or false
bytes<N>binary of exactly N bytes
bytes, stringbinary
T[], T[k]list
(T1,T2,...)tuple

fixed, ufixed, and function types are not supported.

Errors

Following eevm's error rule, encoding raises ArgumentError for an unknown type or a value that doesn't fit its type, since both are mistakes in the caller's code. Decoding returns {:error, :invalid_abi_data} for data that doesn't match its types, since return data and logs come from contracts. Like Solidity's own decoder, decoding is strict: an integer, address, bool, or bytes<N> word with bits outside its type, or an offset or length past the end of the data, is an error rather than being masked or truncated. Bytes after the encoded values are ignored.

Decoding work is capped at 16 times the size of the data, which is far more than any canonical encoding needs. Crafted offsets that point many values at the same bytes, which would make decoding take time quadratic in the input size, fail with {:error, :invalid_abi_data} instead.

Summary

Types

An event log, as in result.logs: topics are integers.

An Elixir value for an ABI type; see the table in the module docs.

Functions

Decodes ABI-encoded data, such as a call's return_data, as values of types. Returns {:ok, values}, one value per type.

Decodes an event log, such as an entry of result.logs, as the event signature, which marks indexed parameters with indexed as Solidity does. Returns {:ok, values} with every parameter's value in declaration order.

ABI-encodes values as the arguments of a function with parameter types, without a selector: the encoding of the tuple of the values.

Builds calldata for a call to the function signature with args: its 4-byte selector followed by the ABI-encoded arguments. Pass the result to EEVM.call/4 or as a transaction's data.

Types

log()

@type log() :: %{
  :topics => [non_neg_integer()],
  :data => binary(),
  optional(atom()) => term()
}

An event log, as in result.logs: topics are integers.

value()

@type value() :: integer() | boolean() | binary() | [value()] | tuple()

An Elixir value for an ABI type; see the table in the module docs.

Functions

decode(types, data)

@spec decode([String.t()], binary()) :: {:ok, [value()]} | {:error, :invalid_abi_data}

Decodes ABI-encoded data, such as a call's return_data, as values of types. Returns {:ok, values}, one value per type.

Returns {:error, :invalid_abi_data} when data doesn't hold valid values of types, and raises ArgumentError for an unknown type.

Examples

iex> EEVM.ABI.decode(["uint256"], <<1000::256>>)
{:ok, [1000]}

iex> data = EEVM.ABI.encode(["int8", "string", "uint16[]"], [-1, "eevm", [1, 2]])
iex> EEVM.ABI.decode(["int8", "string", "uint16[]"], data)
{:ok, [-1, "eevm", [1, 2]]}

iex> EEVM.ABI.decode(["uint8"], <<256::256>>)
{:error, :invalid_abi_data}

decode_log(signature, map)

@spec decode_log(String.t(), log()) ::
  {:ok, [value()]} | {:error, :event_mismatch | :invalid_abi_data}

Decodes an event log, such as an entry of result.logs, as the event signature, which marks indexed parameters with indexed as Solidity does. Returns {:ok, values} with every parameter's value in declaration order.

An indexed uint, int, address, bool, or bytes<N> parameter decodes to its value. Any other indexed parameter (bytes, string, arrays, tuples) decodes to its topic's 32 bytes, the Keccak-256 hash of its encoding, since the log doesn't hold the value itself.

Returns {:error, :event_mismatch} when the log's first topic isn't the event's topic or its topic count doesn't match the indexed parameters (so you can try a log against several events), and {:error, :invalid_abi_data} when the log is that event but its data or topics don't decode. Anonymous events, which have no event topic, aren't supported.

Example

iex> log = %{
...>   address: 0x7070,
...>   topics: [EEVM.event_topic("Transfer(address,address,uint256)"), 0xA11CE, 0xB0B],
...>   data: <<250::256>>
...> }
iex> EEVM.ABI.decode_log("Transfer(address indexed from, address indexed to, uint256 value)", log)
{:ok, [0xA11CE, 0xB0B, 250]}
iex> EEVM.ABI.decode_log("Approval(address indexed,address indexed,uint256)", log)
{:error, :event_mismatch}

encode(types, values)

@spec encode([String.t()], [value()]) :: binary()

ABI-encodes values as the arguments of a function with parameter types, without a selector: the encoding of the tuple of the values.

Raises ArgumentError for an unknown type, a wrong number of values, or a value that doesn't fit its type.

Examples

iex> EEVM.ABI.encode(["uint32", "bool"], [69, true]) |> EEVM.hex()
"0x" <> String.pad_leading("45", 64, "0") <> String.pad_leading("1", 64, "0")

iex> EEVM.ABI.encode(["string"], ["hi"]) |> byte_size()
96

encode_call(signature, args)

@spec encode_call(String.t(), [value()]) :: binary()

Builds calldata for a call to the function signature with args: its 4-byte selector followed by the ABI-encoded arguments. Pass the result to EEVM.call/4 or as a transaction's data.

Raises ArgumentError for a malformed signature, a wrong number of arguments, or an argument that doesn't fit its type.

Examples

iex> calldata = EEVM.ABI.encode_call("balanceOf(address)", [0xA11CE])
iex> EEVM.hex(calldata)
"0x70a08231" <> String.duplicate("0", 59) <> "a11ce"

iex> EEVM.ABI.encode_call("transfer(address to, uint amount)", [0xB0B, 1]) ==
...>   EEVM.ABI.encode_call("transfer(address,uint256)", [0xB0B, 1])
true