Macro alloy_sol_macro::sol

source ·
sol!() { /* proc-macro */ }
Expand description

Generate types that implement alloy-sol-types traits, which can be used for type-safe ABI and EIP-712 serialization to interface with Ethereum smart contracts.

§Examples

Note: the following example code blocks cannot be tested here because the generated code references alloy-sol-types, so they are tested in that crate and included with include_str! in this doc instead.

There are two main ways to use this macro:

Note:

  • relative file system paths are rooted at the CARGO_MANIFEST_DIR environment variable
  • no casing convention is enforced for any identifier,
  • unnamed arguments will be given a name based on their index in the list, e.g. _0, _1
  • a current limitation for certain items is that custom types, like structs, must be defined in the same macro scope, otherwise a signature cannot be generated at compile time. You can bring them in scope with a Solidity type alias.

§Solidity

This macro uses syn-solidity to parse Solidity-like syntax. See its documentation for more.

Solidity input can be either one of the following:

§Attributes

Inner attributes (#![...]) are parsed at the top of the input, just like a Rust module. These can only be sol attributes, and they will apply to the entire input.

Outer attributes (#[...]) are parsed as part of each individual item, like structs, enums, etc. These can be any Rust attribute, and they will be added to every Rust item generated from the Solidity item.

This macro provides the sol attribute, which can be used to customize the generated code. Note that unused attributes are currently silently ignored, but this may change in the future.

List of all #[sol(...)] supported attributes:

  • rpc [ = <bool = false>] (contracts and alike only): generates a structs with methods to construct eth_calls to an on-chain contract through Ethereum JSON RPC, similar to the default behavior of abigen. This makes use of the alloy-contract crate.

    N.B: at the time of writing, the alloy-contract crate is not yet released on crates.io, and its API is completely unstable and subject to change, so this feature is not yet recommended for use.

    Generates:

    • struct {name}Instance<P: Provider> { ... }
      • pub fn new(...) -> {name}Instance<P> + getters and setters
      • pub fn call_builder<C: SolCall>(&self, call: &C) -> SolCallBuilder<P, C>, as a generic way to call any function of the contract, even if not generated by the macro; prefer the other methods when possible
      • pub fn <functionName>(&self, <parameters>...) -> CallBuilder<P, functionReturn> for each function in the contract
      • pub fn <eventName>_filter(&self) -> Event<P, eventName> for each event in the contract
    • pub fn new ..., same as above just as a free function in the contract module
  • abi [ = <bool = false>]: generates functions which return the dynamic ABI representation (provided by alloy_json_abi) of all the generated items. Requires the "json" feature. For:

    • contracts: generates an abi module nested inside of the contract module, which contains:
      • pub fn contract() -> JsonAbi,
      • pub fn constructor() -> Option<Constructor>
      • pub fn fallback() -> Option<Fallback>
      • pub fn receive() -> Option<Receive>
      • pub fn functions() -> BTreeMap<String, Vec<Function>>
      • pub fn events() -> BTreeMap<String, Vec<Event>>
      • pub fn errors() -> BTreeMap<String, Vec<Error>>
    • items: generates implementations of the SolAbiExt trait, alongside the existing alloy-sol-types traits
  • alloy_sol_types = <path = ::alloy_sol_types> (inner attribute only): specifies the path to the required dependency alloy-sol-types.

  • alloy_contract = <path = ::alloy_contract> (inner attribute only): specifies the path to the optional dependency [alloy-contract]. This is only used in combination with the rpc attribute.

  • all_derives [ = <bool = false>]: adds all possible #[derive(...)] attributes to all generated types. May significantly increase compile times due to all the extra generated code. This is the default behavior of abigen

  • extra_methods [ = <bool = false>]: adds extra implementations and methods to all applicable generated types, such as From impls and as_<variant> methods. May significantly increase compile times due to all the extra generated code. This is the default behavior of abigen

  • docs [ = <bool = true>]: adds doc comments to all generated types. This is the default behavior of abigen

  • bytecode = <hex string literal> (contract-like only): specifies the creation/init bytecode of a contract. This will emit a static item with the specified bytes.

  • deployed_bytecode = <hex string literal> (contract-like only): specifies the deployed bytecode of a contract. This will emit a static item with the specified bytes.

  • type_check = <string literal> (UDVT only): specifies a function to be used to check an User Defined Type.

§Structs and enums

Structs and enums generate their corresponding Rust types. Enums are additionally annotated with #[repr(u8)], and as such can have a maximum of 256 variants.

use alloy_primitives::{hex, Address, U256};
use alloy_sol_types::{sol, SolEnum, SolType};

sol! {
   struct Foo {
       uint256 bar;
       address[] baz;
   }

   /// Nested struct.
   struct Nested {
       Foo[2] a;
       address b;
   }

   enum Enum {
       A,
       B,
       C,
   }
}

#[test]
fn structs() {
   let my_foo = Foo {
       bar: U256::from(42),
       baz: vec![Address::repeat_byte(0x11), Address::repeat_byte(0x22)],
   };

   let _nested = Nested { a: [my_foo.clone(), my_foo.clone()], b: Address::ZERO };

   let abi_encoded = Foo::abi_encode_sequence(&my_foo);
   assert_eq!(
       abi_encoded,
       hex! {
           "000000000000000000000000000000000000000000000000000000000000002a" // bar
           "0000000000000000000000000000000000000000000000000000000000000040" // baz offset
           "0000000000000000000000000000000000000000000000000000000000000002" // baz length
           "0000000000000000000000001111111111111111111111111111111111111111" // baz[0]
           "0000000000000000000000002222222222222222222222222222222222222222" // baz[1]
       }
   );

   let abi_encoded_enum = Enum::B.abi_encode();
   assert_eq!(
       abi_encoded_enum,
       hex! {
           "0000000000000000000000000000000000000000000000000000000000000001"
       }
   );
}

§UDVT and type aliases

User defined value types (UDVT) generate a tuple struct with the type as its only field, and type aliases simply expand to the corresponding Rust type.

use alloy_primitives::Address;
use alloy_sol_types::{sol, SolType};

// Type definition: generates a new struct that implements `SolType`
sol! {
   type MyType is uint256;
}

// Type aliases
type B32 = sol! { bytes32 };
// This is equivalent to the following:
// type B32 = alloy_sol_types::sol_data::Bytes<32>;

type SolArrayOf<T> = sol! { T[] };
type SolTuple = sol! { tuple(address, bytes, string) };

#[test]
fn types() {
   let _ = <sol!(bool)>::abi_encode(&true);
   let _ = B32::abi_encode(&[0; 32]);
   let _ = SolArrayOf::<sol!(bool)>::abi_encode(&vec![true, false]);
   let _ = SolTuple::abi_encode(&(Address::ZERO, vec![0; 32], "hello".to_string()));
}

§State variables

Public and external state variables will generate a getter function just like in Solidity.

See the functions and contracts sections for more information.

§Functions and errors

Functions generate two structs that implement SolCall: <name>Call for the function arguments, and <name>Return for the return values.

In the case of overloaded functions, an underscore and the index of the function will be appended to <name> (like foo_0, foo_1…) for disambiguation, but the signature will remain the same.

E.g. if there are two functions named foo, the generated types will be foo_0Call and foo_1Call, each of which will implement SolCall with their respective signatures.

use alloy_primitives::{hex, keccak256, U256};
use alloy_sol_types::{sol, SolCall, SolError};

sol! {
   function foo(uint256 a, uint256 b) external view returns (uint256);

   // These will generate structs prefixed with `overloaded_0`, `overloaded_1`,
   // and `overloaded_2` by default, but each signature is calculated with
   // `overloaded` as the function name.
   function overloaded();
   function overloaded(uint256) returns (uint256);
   function overloaded(string);

   // State variables will generate getter functions just like in Solidity.
   mapping(uint k => bool v) public variableGetter;

   /// Implements [`SolError`].
   #[derive(Debug, PartialEq, Eq)]
   error MyError(uint256 a, uint256 b);
}

#[test]
fn function() {
   assert_call_signature::<fooCall>("foo(uint256,uint256)");

   let call = fooCall { a: U256::from(1), b: U256::from(2) };
   let _call_data = call.abi_encode();

   let _ = overloaded_0Call {};
   assert_call_signature::<overloaded_0Call>("overloaded()");

   let _ = overloaded_1Call { _0: U256::from(1) };
   assert_call_signature::<overloaded_1Call>("overloaded(uint256)");

   let _ = overloaded_2Call { _0: "hello".into() };
   assert_call_signature::<overloaded_2Call>("overloaded(string)");

   // Exactly the same as `function variableGetter(uint256) returns (bool)`.
   let _ = variableGetterCall { k: U256::from(2) };
   assert_call_signature::<variableGetterCall>("variableGetter(uint256)");
   let _ = variableGetterReturn { v: false };
}

#[test]
fn error() {
   assert_error_signature::<MyError>("MyError(uint256,uint256)");
   let call_data = hex!(
       "0000000000000000000000000000000000000000000000000000000000000001"
       "0000000000000000000000000000000000000000000000000000000000000002"
   );
   assert_eq!(
       MyError::abi_decode_raw(&call_data, true),
       Ok(MyError { a: U256::from(1), b: U256::from(2) })
   );
}

fn assert_call_signature<T: SolCall>(expected: &str) {
   assert_eq!(T::SIGNATURE, expected);
   assert_eq!(T::SELECTOR, keccak256(expected)[..4]);
}

fn assert_error_signature<T: SolError>(expected: &str) {
   assert_eq!(T::SIGNATURE, expected);
   assert_eq!(T::SELECTOR, keccak256(expected)[..4]);
}

§Events

Events generate a struct that implements SolEvent.

Note that events have special encoding rules in Solidity. For example, string indexed will be encoded in the topics as its bytes32 Keccak-256 hash, and as such the generated field for this argument will be bytes32, and not string.

#![allow(clippy::assertions_on_constants)]

use alloy_primitives::{hex, keccak256, Bytes, Log, B256, U256};
use alloy_rlp::{Decodable, Encodable};
use alloy_sol_types::{abi::token::WordToken, sol, SolEvent};

sol! {
   #[derive(Default, PartialEq, Debug)]
   event MyEvent(bytes32 indexed a, uint256 b, string indexed c, bytes d);

   event LogNote(
       bytes4   indexed  sig,
       address  indexed  guy,
       bytes32  indexed  foo,
       bytes32  indexed  bar,
       uint              wad,
       bytes             fax
   ) anonymous;

   struct Data {
       bytes data;
   }
   event MyEvent2(Data indexed data);
}

#[test]
fn event() {
   assert_event_signature::<MyEvent>("MyEvent(bytes32,uint256,string,bytes)");
   assert!(!MyEvent::ANONYMOUS);
   let event = MyEvent {
       a: [0x11; 32].into(),
       b: U256::from(1u64),
       c: keccak256("Hello World"),
       d: Bytes::default(),
   };
   // topics are `(SELECTOR, a, keccak256(c))`
   assert_eq!(
       event.encode_topics_array::<3>(),
       [
           WordToken(MyEvent::SIGNATURE_HASH),
           WordToken(B256::repeat_byte(0x11)),
           WordToken(keccak256("Hello World"))
       ]
   );
   // dynamic data is `abi.abi_encode(b, d)`
   assert_eq!(
       event.encode_data(),
       hex!(
           // b
           "0000000000000000000000000000000000000000000000000000000000000001"
           // d offset
           "0000000000000000000000000000000000000000000000000000000000000040"
           // d length
           "0000000000000000000000000000000000000000000000000000000000000000"
       ),
   );

   assert_event_signature::<LogNote>("LogNote(bytes4,address,bytes32,bytes32,uint256,bytes)");
   assert!(LogNote::ANONYMOUS);

   assert_event_signature::<MyEvent2>("MyEvent2((bytes))");
   assert!(!MyEvent2::ANONYMOUS);
}

#[test]
fn event_rlp_roundtrip() {
   let event = MyEvent {
       a: [0x11; 32].into(),
       b: U256::from(1u64),
       c: keccak256("Hello World"),
       d: Vec::new().into(),
   };

   let rlpable_log = Log::<MyEvent>::new_from_event_unchecked(Default::default(), event);

   let mut rlp_encoded = vec![];
   rlpable_log.encode(&mut rlp_encoded);
   assert_eq!(rlpable_log.length(), rlp_encoded.len());

   let rlp_decoded = Log::decode(&mut rlp_encoded.as_slice()).unwrap();
   assert_eq!(rlp_decoded, rlpable_log.reserialize());

   let decoded_log = MyEvent::decode_log(&rlp_decoded, true).unwrap();

   assert_eq!(decoded_log, rlpable_log)
}

fn assert_event_signature<T: SolEvent>(expected: &str) {
   assert_eq!(T::SIGNATURE, expected);
   assert_eq!(T::SIGNATURE_HASH, keccak256(expected));
}

§Contracts/interfaces

Contracts generate a module with the same name, which contains all the items. This module will also contain 3 container enums which implement SolInterface, one for each:

  • functions: <contract_name>Calls
  • errors: <contract_name>Errors
  • events: <contract_name>Events Note that by default only ABI encoding are generated. In order to generate bindings for RPC calls, you must enable the #[sol(rpc)] attribute.
use alloy_primitives::{address, hex, U256};
use alloy_sol_types::{sol, SolCall, SolConstructor, SolInterface};

sol! {
   /// Interface of the ERC20 standard as defined in [the EIP].
   ///
   /// [the EIP]: https://eips.ethereum.org/EIPS/eip-20
   #[derive(Debug, PartialEq, Eq)]
   contract ERC20 {
       mapping(address account => uint256) public balanceOf;

       constructor(string name, string symbol);

       event Transfer(address indexed from, address indexed to, uint256 value);
       event Approval(address indexed owner, address indexed spender, uint256 value);

       function totalSupply() external view returns (uint256);
       function transfer(address to, uint256 amount) external returns (bool);
       function allowance(address owner, address spender) external view returns (uint256);
       function approve(address spender, uint256 amount) external returns (bool);
       function transferFrom(address from, address to, uint256 amount) external returns (bool);
   }
}

#[test]
fn constructor() {
   let constructor_args =
       ERC20::constructorCall::new((String::from("Wrapped Ether"), String::from("WETH")))
           .abi_encode();
   let constructor_args_expected = hex!("00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000d577261707065642045746865720000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000045745544800000000000000000000000000000000000000000000000000000000");

   assert_eq!(constructor_args.as_slice(), constructor_args_expected);
}

#[test]
fn transfer() {
   // random mainnet ERC20 transfer
   // https://etherscan.io/tx/0x947332ff624b5092fb92e8f02cdbb8a50314e861a4b39c29a286b3b75432165e
   let data = hex!(
       "a9059cbb"
       "0000000000000000000000008bc47be1e3abbaba182069c89d08a61fa6c2b292"
       "0000000000000000000000000000000000000000000000000000000253c51700"
   );
   let expected = ERC20::transferCall {
       to: address!("8bc47be1e3abbaba182069c89d08a61fa6c2b292"),
       amount: U256::from(9995360000_u64),
   };

   assert_eq!(data[..4], ERC20::transferCall::SELECTOR);
   let decoded = ERC20::ERC20Calls::abi_decode(&data, true).unwrap();
   assert_eq!(decoded, ERC20::ERC20Calls::transfer(expected));
   assert_eq!(decoded.abi_encode(), data);
}

§JSON ABI

Contracts can also be generated from ABI JSON strings and files, similar to the ethers-rs abigen! macro.

JSON objects containing the abi, evm, bytecode, deployedBytecode, and similar keys are also supported.

Note that only valid JSON is supported, and not the human-readable ABI format, also used by abigen!. This should instead be easily converted to normal Solidity input.

Prefer using Solidity input when possible, as the JSON ABI format omits some information which is useful to this macro, such as enum variants and visibility modifiers on functions.

use alloy_sol_types::{sol, SolCall};

sol!(
   MyJsonContract1,
   r#"[
       {
           "inputs": [
               { "name": "bar", "type": "uint256" },
               { 
                   "internalType": "struct MyJsonContract.MyStruct",
                   "name": "baz",
                   "type": "tuple",
                   "components": [
                       { "name": "a", "type": "bool[]" },
                       { "name": "b", "type": "bytes18[][]" }
                   ]
               }
           ],
           "outputs": [],
           "stateMutability": "view",
           "name": "foo",
           "type": "function"
       }
   ]"#
);

// This is the same as:
sol! {
   interface MyJsonContract2 {
       struct MyStruct {
           bool[] a;
           bytes18[][] b;
       }

       function foo(uint256 bar, MyStruct baz) external view;
   }
}

#[test]
fn abigen() {
   assert_eq!(MyJsonContract1::fooCall::SIGNATURE, MyJsonContract2::fooCall::SIGNATURE,);
}