Skip to content
LogoLogo

ABI encode and decode

Abi provides Solidity's encoding helpers.

abi.lithe
(* `Abi` mirrors Solidity's `abi.*` helpers. *)
 
import "std/abi";
import "std/evm";
 
interface Token is {
  function transfer(to : address; amount : uint) : bool;
}
 
storage is record {
  last : bytes;
}
 
external pure function encode(const to : address; const amount : uint) : bytes is
  Abi.encode(to, amount)
 
external pure function decode(const data : bytes) : uint is
  Abi.decode(data, uint)
 
external pure function packed(const tag : string; const who : address) : bytes is
  Abi.encodePacked(tag, who)
 
(* Calldata for `transfer(address,uint256)`, three ways. *)
external pure function bySelector(const to : address; const amount : uint) : bytes is
  Abi.encodeWithSelector(0xa9059cbb, to, amount)
 
external pure function bySignature(const to : address; const amount : uint) : bytes is
  Abi.encodeWithSignature("transfer(address,uint256)", to, amount)
 
external pure function byCall(const to : address; const amount : uint) : bytes is
  Abi.encodeCall(Token.transfer, to, amount)
 
(* The raw calldata of the current call. *)
external function remember() is {
  storage.last := Evm.calldata();
}
  • Abi.decode takes a type name as its second argument.
  • bySelector, bySignature, and byCall produce identical bytes. Only encodeCall checks the arguments against the method's parameter types.

Several values, and nested ones

A tuple of type names decodes one value of each, as Solidity's abi.decode(data, (A, B)) does. Lists and records nest to any depth, both ways:

nested.lithe
import "std/abi";
 
type Order is record {
  id : uint;
  note : string;
  legs : list(uint);
}
 
type Orders is list(Order);
 
type Transfer is record {
  to : address;
  amount : uint;
  memo : string;
}
 
storage is record {
  n : uint;
}
 
external pure function unpack(const data : bytes) : Transfer is {
  const (to, amount, memo) : (address, uint, string) := Abi.decode(data, (address, uint, string));
} with { to = to; amount = amount; memo = memo; }
 
external pure function roundTrip(const os : list(Order)) : list(Order) is
  Abi.decode(Abi.encode(os), Orders)
 
external pure function secondLeg(const data : bytes) : uint is {
  const os : list(Order) := Abi.decode(data, Orders);
} with os[0].legs[1]
  • A record is one Solidity tuple wherever it is a value: Abi.encode(os) is abi.encode(Order[]).
  • Decoding checks every offset, length, and word as Solidity's decoder does, and malformed data reverts.

Calldata and answers of your own

With a raw call, you build the calldata and decode the answer yourself:

raw.lithe
import "std/abi";
import "std/evm";
 
interface Token is {
  view function balanceOf(owner : address) : uint;
}
 
storage is record {
  n : uint;
}
 
external view function balance(const token : address; const owner : address) : uint is
  Abi.decode(query Evm.staticCall(token, Abi.encodeCall(Token.balanceOf, owner)), uint)
 
external view function reserves(const pair : address) : (uint112, uint112) is {
  const answer : bytes := query Evm.staticCall(pair, Abi.encodeWithSignature("getReserves()"));
  const (r0, r1, last) : (uint112, uint112, uint32) := Abi.decode(answer, (uint112, uint112, uint32));
} with (r0, r1)

examples/abi_codec.lithe and examples/raw_codec.lithe go further, and bench/codec and bench/dialer compare the same patterns against Solidity, byte for byte.