Skip to main content
Version: v6
Attention!You are viewing unreleased ink! 6 docs. Click here to view the latest docs.

Solidity Title Picture

ink! vs. Solidity

The following table gives a brief comparison of features between ink! and Solidity:

ink!Solidity
Virtual MachinePolkaVMEVM
EncodingSCALE or Solidity ABISolidity ABI
LanguageRustStandalone
Overflow ProtectionEnabled by defaultYes
Constructor FunctionsMultipleSingle
ToolingMost tools that support RustCustom
VersioningSemanticSemantic
Has Metadata?YesYes
Multi-File ProjectYesYes
Storage EntriesVariable256 bits
Supported TypesDocsDocs
Has Interfaces?Yes (Rust Traits)Yes

Converting a Solidity Contract to ink!

In the following, we'll explain how to convert a Solidity contract to ink!.

1. Generate a new ink! contract

Run the following command to generate the skeleton for an ink! contract. The command will set up the boilerplate code for ink!'s "Hello, World!" (the flipper contract)).

cargo contract new <contract-name>

2. Build the contract

cargo contract build

3. Convert Solidity class fields to Rust struct

Solidity is an object-oriented language, and uses classes. ink! (Rust) does not use classes.

An example Solidity class looks like:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.1;

contract MyContract {
bool private _theBool;
event UpdatedBool(bool indexed _theBool);

constructor(bool theBool) {
require(theBool == true, "theBool must start as true");

_theBool = theBool;
}

function setBool(bool newBool) public returns (bool boolChanged) {
if (_theBool == newBool) {
boolChanged = false;
} else {
boolChanged = true;
}

_theBool = newBool;

// emit event
emit UpdatedBool(newBool);
}
}

And the equivalent contract in ink! looks like:

#![cfg_attr(not(feature = "std"), no_std, no_main)]

#[ink::contract]
mod mycontract {
#[ink(storage)]
pub struct MyContract {
the_bool: bool, // class members become struct fields
}

#[ink(event)]
pub struct UpdatedBool {
#[ink(topic)] // -> indexed
the_bool: bool,
}

impl MyContract {
#[ink(constructor)]
pub fn new(the_bool: bool) -> Self {
assert!(the_bool == true, "the_bool must start as true");
Self { the_bool }
}

#[ink(message)] // functions become struct implementations
pub fn set_bool(&mut self, new_bool: bool) -> bool {
let bool_changed: bool;

if self.the_bool == new_bool{
bool_changed = false;
}else{
bool_changed = true;
}

self.the_bool = new_bool;

self.env().emit_event(UpdatedBool {
the_bool: new_bool
});

// return
bool_changed
}
}
}

A few key differences are:

  • Solidity class variables / members will be placed in the contract struct in ink!
  • All class methods in Solidity are implemented for the contract struct in ink!
  • Solidity frequently prefixes variables with an underscore (_name). ink! / Rust only prefixes with an underscore for unused variables.
  • Solidity uses camelCase. ink! uses snake_case.
  • In Solidity, the variable type comes before the variable name (e.g. bool myVar). While ink! specifies var type after the var name (e.g. my_var: bool)

4. Convert each function

  • Start converting each function one by one.
    • A recommended approach is to, if possible, skip cross-contract calls at first and use mock data instead
    • This way off-chain unit tests can be written to test the core functionality
      • unit tests are off-chain and do not work with cross-contract calls
    • Once fully tested, start adding in cross-contract calls and perform on-chain manual + integration tests
  • Ensure that function's visibility (public, private) are matched in ink!
  • In Solidity, if a function returns a bool success, ink! will use a Result<()> instead (Result::Ok or Result::Err).

Solidity return example:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.1;

contract Example {
uint128 public data;

constructor(){}

function setData(uint128 newData) public returns (
bool success,
string memory reason
) {

if (newData == 0) {
return (false, "Data should not be zero");
}

data = newData;
return (true, "");
}
}

The equivalent contract in ink!:

#![cfg_attr(not(feature = "std"), no_std, no_main)]

#[ink::contract]
mod example {
#[ink(storage)]
pub struct Example {
data: u128,
}

#[ink::scale_derive(Encode, Decode, TypeInfo)]
#[derive(Debug, PartialEq, Eq)]
pub enum Error {
DataShouldNotBeZero,
}

pub type Result<T> = core::result::Result<T, Error>;

impl Example {
#[ink(constructor)]
pub fn new() -> Self {
Self { data: 0 }
}

#[ink(message)]
pub fn set_data(&mut self, new_data: u128) -> Result<()> {
if new_data == 0 {
return Err(Error::DataShouldNotBeZero);
}

self.data = new_data;
Ok(())
}
}
}

Best Practices + Tips

  • If the Solidity contract uses a string, it is recommended to use a Vec<u8> to avoid the overhead of a String. See here for more details on why. The smart contract should only contain the information that strictly needs to be placed on the blockchain and go through consensus. The UI should be used for displaying strings.
  • Double check all .unwrap()s performed. Solidity does not have as strict checking as ink! does. For example, a mapping field can be accessed as simple as myMapping[someKey]. ink!, however, requires self.my_mapping.get(some_key).unwrap(). A useful way to handle None cases is to use .unwrap_or(some_val).
  • Run the contracts node with ink-node -lerror,runtime::contracts=debug for debug prints, and errors to be displayed in the nodes console.
  • Just as in Solidity, ink! does not have floating point numbers due to the non-deterministic nature. Instead, the frontend should add decimal points as needed.

Syntax Equivalencies

public function

// solidity
function fnName() public {}
// or
// by default, functions are public
function fnName() {}
// ink!
#[ink(message)]
pub fn fn_name(&self) {}

mapping declaration

// solidity
mapping(address => uint128) private mapName;
//ink!
use ink::storage::Mapping;

#[ink(storage)]
pub struct ContractName {
map_name: Mapping<AccountId, u128>,
}

mapping usage

// solidity

// insert / update
aMap[aKey] = aValue;

// get
aMap[aKey]
// ink!

//insert / update
self.a_map.insert(&a_key, &a_value);

// get
self.a_map.get(a_key).unwrap()

struct

// solidity
struct MyPerson{
address person;
u64 favNum;
}
// ink!
struct MyPerson {
person: AccountId,
fav_num: u64,
}

assertions / requires

// solidity
require(someValue < 10, "someValue is not less than 10");
// ink!
assert!(some_value < 10, "some_value is not less than 10");

timestamp

// solidity
block.timestamp
// ink!
self.env().block_timestamp()

contract caller

// solidity
address caller = msg.sender;
// ink!
let caller: AccountId = self.env().caller();

contract's address

// solidity
address(this)
// ink!
self.env().account_id()

bytes

Solidity has a type bytes. bytes is (essentially) equivalent to an array of uint8. So, bytes in Solidity => Vec<u8> or [u8; ...] in ink!. See here for more details. If desired, a bytes struct can be created in ink! to replicate the bytes type in Solidity.

uint256

Solidity uses uint256 and uint to represent a 256-bit type.

Solidity is 256-bit / 32-byte word optimized. Meaning, using uint256 in Solidity contracts will reduce gas usage -- but increase storage usage. The largest size ink! has built in is a u128. ink! compiles to Wasm. The largest primitive Wasm has is 64bit (due to most computers using 64bit). So, there is no benefit to using any larger primitive over a collection.

When porting a uint256 from Solidity to ink!, it is recommended to, with discretion, determine the range of the value, and choose the appropriate size (u8, u16, u32, u64, u128). If a 256-bit hash value is required, ink! has a Hash primitive available. In the event a value needs to be 256-bit, it is recommended to use an array (e.g. [u64; 4]).

payable

// solidity
function myFunction() payable returns (uint64) {}
#[ink(message, payable)]
pub fn my_function(&self) -> u64 {}

received deposit / payment

// solidity
msg.value
// ink!
self.env().transferred_value()

contract balance

// solidity
address(this).balance
// ink!
self.env().balance()

transfer tokens from contract

// solidity
recipient.send(amount)
// ink!
if self.env().transfer(recipient, amount).is_err() {
panic!("error transferring")
}

events & indexed

// solidity

event MyCoolEvent(
u128 indexed indexedValue,
u128 notIndexedValue,
);

// emit event
emit MyCoolEvent(someValue, someOtherValue);
// ink!

#[ink(event)]
pub struct MyCoolEvent {
#[ink(topic)]
indexed_value: u128,

not_indexed_value: u128,
}

// emit event
self.env().emit_event(MyCoolEvent {
indexed_value: some_value,
not_indexed_value: some_other_value
});

errors and returning

Solidity has several error handling mechanisms: assert, require, revert, and throw. Each of these will revert the changed state when called. See this article for details on these.

ink! uses a Result enum (Ok(T), Err(E)), assert! and panic!. This Stack Exchange answer and GitHub discussion provide more details on these.

throw

Throw is deprecated in Solidity and would throw an invalid opcode error (no details) and revert the state. As an alternative to the if...{throw;} pattern in Solidity, a Result::Err should be returned for expected errors, and an assert! should be used for errors that should not occur.

assert

In Solidity, assert is used as internal guards against errors in the code. In general, properly functioning code should never hit a failing assert. assert in Solidity does not have error strings. In ink!, use assert!. assert! will panic! if it evaluates to false. The state will be reverted, and a CalleeTrapped will be returned. The (optional) error string will be printed to the debug buffer.

// ink!
assert!(caller == owner, "caller is not owner")

require and revert

In Solidity, require is used for general (normal) errors -- such as errors that occur based on user input. require does have the option for an error string. revert is very similar to require except that revert will be called in if ... else chains. Both require and revert will revert the chain state. In ink!, if ... { return Err(Error::SomeError) } should be used for require or revert. When a Result::Err is returned in ink!, then all state is reverted.

In general, Result::Err should be used when a calling contract needs to know why a function failed. Otherwise, assert! should be used as it has less overhead than a Result.

// Solidity
function myFunction(bool returnError) public pure {
require(!returnError, "my error here");

// or

if returnError {
revert("my error here");
}
}
// ink!

#[derive(Debug, PartialEq, Eq)]
#[ink::scale_derive(Encode, Decode, TypeInfo)]
pub enum Error {
/// Provide a detailed comment on the error
MyError,
}

// result type
pub type Result<T> = core::result::Result<T, Error>;

// ...

#[ink(message)]
pub fn my_function(&self, return_error: bool) -> Result<()> {
if return_error{
return Err(Error::MyError)
}
Ok(())
}

cross-contract calling

See here to learn the different ways to do cross-contract calling

submit generic transaction / dynamic cross-contract calling

invokes function found at callee contract address, sends the transferAmount to the callee, and the transactionData payload.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.1;

contract CallContract {

constructor() {}

function invokeTransaction(
address payable callee,
uint transferAmount,
bytes4 functionSelector,
string memory transactionData
) public returns(bool success, bytes memory message) {

bytes memory _data = abi
.encodePacked(functionSelector, transactionData);

(success, message) = callee
.call{value: transferAmount}(_data);

return (success, message);
}
}

The equivalant in Ink!:

#![cfg_attr(not(feature = "std"), no_std, no_main)]

#[ink::contract]
mod call_contract {
use ink::{
env::call::{
build_call,
Call,
ExecutionInput,
Selector
},
prelude::vec::Vec,
};

#[ink(storage)]
#[derive(Default)]
pub struct CallContract {}

#[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub enum Error {
TransactionFailed,
}
type Result<T> = core::result::Result<T, Error>;


impl CallContract{
#[ink(constructor)]
pub fn new() -> Self {
Default::default()
}

#[ink(message, payable)]
pub fn invoke_transaction(
&mut self,
callee: AccountId,
transfer_amount: u128,
function_selector: [u8; 4],
transaction_data: Vec<u8>,
gas_limit: Option<u64>,
) -> Result<()> {

let transaction_result = build_call::<<Self as ::ink::env::ContractEnv>::Env>()
.call_type(
Call::new(callee) // contract to call
.gas_limit(gas_limit.unwrap_or_default())
.transferred_value(transfer_amount), // value to transfer with call
)
.exec_input(
ExecutionInput::new(Selector::new(function_selector))
.push_arg(transaction_data), // SCALE-encoded parameters
)
.returns::<()>()
.try_invoke();

match transaction_result {
Ok(Ok(_)) => Ok(()),
_ => Err(Error::TransactionFailed),
}
}
}
}

Note: the function_selector bytes can be found in the generated target/ink/<contract-name>.json.

Troubleshooting Errors

  • "failed to load bitcode of module '...' "

This happens when trying to import a contract for cross-contract calling.

Solution
Ensure that the following is added to Cargo.toml contract import:`

features = ["ink-as-dependency"]

so the import would look like:

mycontract = { path = "mycontract/", default-features = false, features = ["ink-as-dependency"]}

unit testing (off-chain)

  • Unit tests are an integral part of smart-contract development and ensuring your code works off-chain before testing on-chain.
  • To run ink! tests, use the command cargo test. Add the --nocapture flag for debug prints to show.
  • From the contract module, make sure to make the contract struct and anything else that is going to be used in the unit tests public. For example:
// top of file
#![cfg_attr(not(feature = "std"), no_std, no_main)]


pub use self::mycontract::{
MyContract
};
  • For more complex testing that requires a running node, such as cross-contract calls,please refer to the showcased example here
  • useful code to interact and modify the contract environment for testing

ink_env docs

// get the default accounts (alice, bob, ...)
let accounts = ink::env::test::default_accounts::<ink::env::DefaultEnvironment>();
accounts.alice //usage example

// set which account calls the contract
ink::env::test::set_caller::<ink::env::DefaultEnvironment>(accounts.bob);

// get the contract's address
let callee = ink::env::account_id::<ink::env::DefaultEnvironment>();

// set the contracts address.
// by default, this is alice's account
ink::env::test::set_callee::<ink::env::DefaultEnvironment>(callee);

// transfer native currency to the contract
ink::env::test::set_value_transferred::<ink::env::DefaultEnvironment>(2);

// increase block number (and block timestamp).
// this can be placed in a loop to advance the block many times
ink::env::test::advance_block::<ink::env::DefaultEnvironment>();

// generate arbitrary AccountId
AccountId::from([0x01; 32]);

// generate arbitrary Hash
Hash::from([0x01; 32])

// macro for tests that are expected to panic.
#[should_panic]