Solidity & MetaMask Compatibility
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::None
is mapped to(false, <default_value>)
where<default_value>
is the zero bytes only representation ofT
(e.g.0u8
foru8
orVec::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[] |
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::Ok
variant, whereT
implementsSolEncode
,T
is encoded as "normal" Solidity ABI return data. - When returning the
Result::Err
variant,E
must implementSolErrorEncode
, ink! will set the revert flag in the execution environment, andE
will be encoded as Solidity revert error data, with the error data representation depending on theSolErrorEncode
implementation. - Similarly, for decoding,
T
must implementSolDecode
, whileE
must 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.E
if the revert flag is set, andT
otherwise).
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
use ink::{SolErrorDecode, SolErrorEncode};
// Represented as a Solidity custom error with no parameters
#[derive(SolErrorDecode, SolErrorEncode)]
struct UnitError;
// Represented as a Solidity custom error with parameters
#[derive(SolErrorDecode, SolErrorEncode)]
struct ErrorWithParams(bool, u8, String);
// Represented as a Solidity custom error with named parameters
#[derive(SolErrorDecode, SolErrorEncode)]
struct ErrorWithNamedParams {
status: bool,
count: u8,
reason: String,
}
// Represented as multiple Solidity custom errors
// (i.e. each variant represents a Solidity custom error)
#[derive(SolErrorDecode, SolErrorEncode)]
enum MultipleErrors {
UnitError,
ErrorWithParams(bool, u8, String),
ErrorWithNamedParams {
status: bool,
count: u8,
reason: String,
}
}
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
SolEncode
andSolDecode
respectively, 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.
MetaMask
You can use MetaMask to interact with your ink!
smart contract via the Solidity ABI.
To set up your wallet and connect to the appropriate network, follow this quick start guide: Connect MetaMask to Polkadot Hub Testnet
Network name: Polkadot Hub TestNet
Currency symbol: PAS
Chain ID: 420420422
RPC URL: https://testnet-passet-hub-eth-rpc.polkadot.io
Block explorer URL: https://blockscout-passet-hub.parity-testnet.parity.io/
For step-by-step manual configuration instructions, see this guide: Connect MetaMask to Polkadot Hub Testnet.
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
This guide walks through compiling an ink!
contract with Solidity metadata, configuring Hardhat, deploying to the Polkadot Hub Testnet, and interacting with the contract using Ethers.js.
Block explorers
PolkaVM smart contracts are compatible with Ethereum-style block explorers such as BlockScout, which is already integrated with the Polkadot Hub Testnet.
For additional information and instructions, check out: Polkadot Smart Contract Block Explorers