Rust/ink! to Solidity ABI type mapping
This mapping is defined using the SolEncode and SolDecode traits,
which are analogs to scale::Encode and scale::Decode
(but for Solidity ABI encoding/decoding).
You won't be able to use Rust types for which no mapping to a Solidity type is defined.
An error about a missing trait implementation for this type will be thrown.
Default/provided mappings
SolEncode and SolDecode are implemented
for the following Rust/ink! primitive types creating a mapping
to the corresponding Solidity ABI types as shown in the table below:
| Rust/ink! type | Solidity ABI type | Notes |
|---|---|---|
bool | bool | |
iN for N ∈ {8,16,32,64,128} | intN | e.g i8 ↔ int8 |
uN for N ∈ {8,16,32,64,128} | uintN | e.g u8 ↔ uint8 |
ink::U256 | uint256, uint | uint is just an alias of uint256 in Solidity |
String | string | |
Box<str> | string | |
ink::Address / ink::H160 | address | ink::Address is a type alias for the ink::H160 type used for addresses in pallet-revive |
[T; N] for const N: usize | T[N] | e.g. [i8; 64] ↔ int8[64] |
Vec<T> | T[] | e.g. Vec<i8> ↔ int8[] |
Box<[T]> | T[] | e.g. Box<[i8]> ↔ int8[] |
ink::sol::FixedBytes<N> for 1 <= N <= 32 | bytesN | e.g. FixedBytes<32> ↔ bytes32, FixedBytes<N> is just a newtype wrapper for [u8; N] that also implements From<u8> |
ink::sol::DynBytes | bytes | DynBytes is just a newtype wrapper for Vec<u8> that also implements From<Box<[u8]>> |
(T1, T2, T3, ... T12) | (U1, U2, U3, ... U12) | where T1 ↔ U1, ... T12 ↔ U12 e.g. (bool, u8, Address) ↔ (bool, uint8, address) |
Option<T> | (bool, T) | e.g. Option<u8> ↔ (bool, uint8) |
In Solidity ABI encoding, uint8[] and uint8[N] are encoded differently from
bytes and bytesN. In Rust/ink!, Vec<u8> and [u8; N] are mapped to Solidity's
uint8[] and uint8[N] representations, so there's a need for dedicated Rust/ink! types
(i.e. ink::sol::DynBytes and ink::sol::FixedBytes<N>)
that map to Solidity's bytes and bytesN representations.
Rust's Option<T> type doesn't have a semantically equivalent Solidity ABI type,
because Solidity enums are field-less.
So Option<T> is mapped to a tuple representation instead (i.e. (bool, T)),
because this representation allows preservation of semantic information in Solidity,
by using the bool as a "flag" indicating the variant
(i.e. false for None and true for Some) such that:
Option::Noneis mapped to(false, <default_value>)where<default_value>is the zero bytes only representation ofT(e.g.0u8foru8orVec::new()forVec<T>)Option::Some(value)is mapped to(true, value)
The resulting type in Solidity can be represented as a struct with a field for the "flag" and another for the data.
Note that enum in Solidity is encoded as uint8 in Solidity ABI encoding,
while the encoding for bool is equivalent to the encoding of uint8,
with true equivalent to 1 and false equivalent to 0.
Therefore, the bool "flag" can be safely interpreted as a bool or enum (or even uint8)
in Solidity code.
SolEncode is additionally implemented for reference and smart
pointer types below:
| Rust/ink! type | Solidity ABI type | Notes |
|---|---|---|
&str, &mut str | string | |
&T, &mut T, Box<T> | T | e.g. &i8 ↔ int8 |
&[T], &mut [T] | T[] | e.g. &[i8] ↔ int8[] |
ink::sol::ByteSlice | bytes | ByteSlice is a just newtype wrapper for &[u8] |
Handling the Result<T, E> type
Rust's Result<T, E> type doesn't have a semantically equivalent Solidity ABI type,
because Solidity enums are field-less, so no composable mapping is provided.
However, Result<T, E> types are supported as the return type of messages
and constructors, and they're handled at language level as follows:
- When returning the
Result::Okvariant, whereTimplementsSolEncode,Tis encoded as "normal" Solidity ABI return data. - When returning the
Result::Errvariant,Emust implementSolErrorEncode, ink! will set the revert flag in the execution environment, andEwill be encoded as Solidity revert error data, with the error data representation depending on theSolErrorEncodeimplementation. - Similarly, for decoding,
Tmust implementSolDecode, whileEmust implementSolErrorDecode, and the returned data is decoded asT(i.e.Result::Ok) orE(i.e.Result::Err) depending on whether the revert flag is set (i.e.Eif the revert flag is set, andTotherwise).
The SolErrorEncode and SolErrorDecode traits define
the highest level interfaces for encoding and decoding an arbitrary Rust/ink! error type as
Solidity ABI revert error data.
Default implementations for both SolErrorEncode and SolErrorDecode are provided for unit
(i.e. ()), and these are equivalent to reverting with no error data in Solidity
(i.e. empty output buffer).
For arbitrary custom error types, Derive macros are provided for automatically generating
implementations of SolErrorEncode and SolErrorDecode for structs and enums for which
all fields (if any) implement SolEncode and SolDecode.
- For structs, the struct name is used as the name of the Solidity custom error while the fields (if any) are the parameters
- For enums, each variant is its own Solidity custom error, with the variant name being the custom error name, and the fields (if any) being the parameters
For convenience, the #[ink::error] attribute macro is also provided for automatically deriving the following traits:
SolErrorEncode: for encoding a custom type as revert error dataSolErrorDecode: for decoding revert error data into a custom typeSolErrorMetadata: for generating Solidity ABI metadata (gated behind thestdfeature)
// Represented as a Solidity custom error with no parameters
#[ink::error]
struct UnitError;
// Represented as a Solidity custom error with parameters
#[ink::error]
struct ErrorWithParams(bool, u8, String);
// Represented as a Solidity custom error with named parameters
#[ink::error]
struct ErrorWithNamedParams {
status: bool,
count: u8,
reason: String,
}
// Represented as multiple Solidity custom errors
// (i.e. each variant represents a Solidity custom error)
#[ink::error]
enum MultipleErrors {
UnitError,
ErrorWithParams(bool, u8, String),
ErrorWithNamedParams {
status: bool,
count: u8,
reason: String,
}
}
In "ink" and "all" ABI mode, the #[ink::error] attribute macro
will also derive implementations of the scale::Encode and scale::Decode traits.
For other Solidity revert error data representations (e.g. legacy revert strings),
you can implement SolErrorEncode and SolErrorDecode
manually to match those representations.
Rust's coherence/orphan rules mean that you can only implement the
SolErrorEncode and SolErrorDecode traits for local types.
However, one way around this limitation is to use the newtype pattern.
Mappings for arbitrary custom types
For arbitrary custom types, Derive macros are provided for automatically generating
implementations of SolEncode and SolDecode
- For structs where all fields (if any) implement
SolEncodeandSolDecoderespectively, including support for generic types - For enums where all variants are either unit-only or field-less (see notes below for the rationale for this limitation)
use ink_macro::{SolDecode, SolEncode};
#[derive(SolDecode, SolEncode)]
struct UnitStruct;
#[derive(SolDecode, SolEncode)]
struct TupleStruct(bool, u8, String);
#[derive(SolDecode, SolEncode)]
struct FieldStruct {
status: bool,
count: u8,
reason: String,
}
#[derive(SolDecode, SolEncode)]
enum SimpleEnum {
One,
Two,
Three,
}
#[derive(SolDecode, SolEncode)]
struct NestedStruct {
unit: UnitStruct,
tuple: TupleStruct,
fields: FieldStruct,
enumerate: SimpleEnum,
}
#[derive(SolDecode, SolEncode)]
struct GenericStruct<T> {
concrete: u8,
generic: T,
}
Solidity has no semantic equivalent for Rust/ink! enums with fields (i.e. Solidity enums can only express the equivalent of Rust unit-only or field-less enums).
So mapping complex Rust enums (i.e. enums with fields) to "equivalent" Solidity representations typically yields complex structures based on tuples (at Solidity ABI encoding level) and structs (at Solidity language level).
Because of this, the Derive macros for SolEncode and SolDecode do NOT generate implementations
for enums with fields.
However, you can define custom representations for these types by manually implementing
the SolEncode and SolDecode
(see linked rustdoc for instructions).
Rust's coherence/orphan rules mean that you can only implement the
SolEncode and SolDecode traits for local types.
However, one way around this limitation is to use the newtype pattern.
Next Steps
- Learn how to call Solidity contracts from ink!
- Explore Solidity tooling integration with MetaMask, Hardhat, and more