First, read our blogpost ink! speaks Solidity on PolkaVM.
With ink! v6, we have introduced an abi field in a custom ink-lang table
in the package.metadata table of a contract's manifest
file (i.e. the Cargo.toml file) - more details here.
It allows building your contract in Solidity ABI compatibility mode
when declared as follows:
[package.metadata.ink-lang]
abi = "sol"
The implication of supporting Solidity ABI encoding is that all types used as constructor/message argument and return types, and event argument types must define a mapping to an equivalent Solidity ABI type.
This is similar to the requirement to implement scale::Encode and scale::Decode
for Rust types used in the public interfaces of ink!/"native" ABI encoded contracts.
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 | |
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) |
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.
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.
Solidity Tooling
You can deploy and interact with ink! smart contracts using popular Solidity tools like Hardhat and Foundry thanks to the Solidity-compatible ABI output.
Full Tutorial: Use Solidity Tooling with ink! Contracts