Introduction to Solidity types
One of the first important steps in learning Solidity, a statically-typed language, is to get a good understanding of the types available to use. So here they are!
Note: This was written for the latest Solidity version at the time of writing which is 0.8.1.
Value Types
Variables with value types hold data directly and passing them as argument or assignment creates a copy. Let's start with the simplest of them!
Boolean
They are declared with the bool
keyword and can be either true
or false
bool myBoolean; // declaration
myBoolean = true; // assignment
Integer
You can specify if a variable is a signed integer with int
or unsigned with uint
followed by their range by appending the number of bits needed.
For example uint8
represents an unsigned integer of 8 bits.
It goes in increment of 8 from int8
to int256
and from uint8
to uint256
.
💡 If you simply declare an int
or uint
it will take the maximum range by default of 256 bits.
Integer Literal
Interpreted as decimals, integer literals are comprised of numbers. Ex: 69, 88, 11...
Decimals are possible such as .3
, 1.6
.
And so is scientific notation: 3e10
, -4.3e-2
💡 For better readability integers can be written with an underscore, i.e 456_000
and 456000
are both valid and equivalent.
String Literal
String literals can either be written with single ('unicorn'
) or double quotes ("sushi"
).
💡 "a long" " text"
is considered the same as "a long text"
So what if you actually want to have a string with quotes in it? You need to escape them. Escaping a character is done by adding \
in front. So '\"hello!\"'
becomes "hello!"
. Other characters that need to be escaped include newline, backslash, carriage return etc...
💡 By default string literals can only contain ASCII but unicode literals with UTF-8 encoding can be written like this:
string memory congrats = unicode"Congrats! 🎉"
Hexadecimal Literal
Hexadecimal literals are recognizable as they start with the prefix hex
such as hex"0d2c"
. They behave a lot like string literals.
Enum
An enum
is a convenient way to declare your own type with a limited, well-defined list of options:
enum CustomerType { EarlyBird, Regular, VIP }
CustomerType customer;
Behind the scenes, options are just integers starting from 0 for the first option (which is also the enum's default value). Hence, enums are explicitly convertible from and to integers.
Address
Here's where we are starting to see how Solidity was uniquely designed to write smart contracts.
They are 2 ways to declare an address:
address plainAddress;
address payable payableAddress;
The difference is that an address payable
is meant to receive Ether through a method such as .transfer()
which is not available on a "simple" address
.
One being a sort of extension of the other, it makes sense that you can cast an address payable to an address implicitly...
address myAddress = payableAddress;
...But the opposite has to be done explicitly.
address payable foo = payable(plainAddress);
You can learn more about the possible conversions here.
The balance of an address can be obtained by simply writing myAddress.balance
. For the current contract's balance, you first convert it to an address address(this)
so it becomes: address(this).balance
Address Literal
It's an hexadecimal literal such as 0xBA2E7Fed597fd0E3e70f5130BcDbbFE06bB94fe1
that matches the right format of an Ethereum address. It has to pass a checksum test to be hardcoded in a contract like this:
address deployedContract = 0xBA2E7Fed597fd0E3e70f5130BcDbbFE06bB94fe1;
It has the type address
and not address payable
starting from Solidity 0.8 but can be converted to the latter.
Contract
Contracts have their own type which can be converted:
- to a contract they inherit from
- to or from an address:
address contractAddress = address(MyContract)
⚠️ There are some subtleties in case you want to cast to an address payable
and the contract does not have a receive or payable fallback function, you need to use payable(address(x))
.
On a contract type, the members are the external functions (myContract.getReward(args)
) as well as all the public variables defined in it (myContract.foo
): Solidity automatically creates a getter function of the same name for any variable marked as public
.
Fixed-size byte array
If you want to store a sequence of bytes which you know won't change in size you can use one of the following types: byte
(= bytes1
), bytes2
, bytes3
, ..., bytes32
. Pick the right size!
Function
Functions are defined this way:
function myFunction(<parameter types>) {private|internal|external|public} [pure|view|payable] [returns (<return types>)]
Let's unpack the few different options!
A function declaration starts with the function
keyword, followed by the function name and then by parentheses which, optionally, contain parameters in between them such as transfer(address to, uint value)
You then need to specify and pick between
private vs. internal vs. external vs. public
Private functions can only be called from other functions inside the current contract. Their name generally start with _
by convention.
Internal functions are like private
functions but can also be executed from contracts deriving from the contract they were defined in.
External functions are meant to be called from outside the contract declaring them. (You can only call them internally by using this.foo()
instead of foo()
)
Public functions can be called from anywhere, both internally and externally.
⚠️ Needless to say that getting this right is important from a security perspective. It's good practice to restrict visibility as much as possible, only exposing what needs to be exposed. With newer versions of Solidity, a function's visibility must be declared. Using an older version and not specifying any of these keywords means opting for the default function visibility which is public
.
pure vs. view vs. payable
payable
: include this function type if you want your function to be able to receive ETH while being called.
(Note that it doesn't mean it must be sent some eth to work. Depending on your implementation it can really well function with a payment of 0 ETH)view
: this function modifier ensures that the function does not modify the state, basically it doesn't save or change anything on the ethereum blockchainpure
: pure functions can neither save nor read data from the blockchain
💡 Marking a function with view
or pure
means that it doesn't cost any gas to call it from outside the current contract but does when called internally!
Function return types
Unless the function returns nothing you must state what types are returned:
uint public minimumContribution;
uint public contributorCount;
address public manager;
// ...
function getSummary() public view returns (uint, uint, address) {
return (
minimumContribution,
contributorCount,
manager
);
}
Reference Types
As opposed to value types, data location of a reference type must be declared: it corresponds to where the data is stored. There are 3 options:
- storage: This is where to store permanent data written on the blockchain. It's the more expensive data location. Data in
storage
for a smart contract is like data in a file on your hard drive, it will exists as long the contract does. - memory: The
memory
location is for data that will be held temporarily during a function execution and discarded after. Data inmemory
is cheaper and analogous to data stored in RAM. - calldata:
calldata
is a special data location where function arguments are stored. This data is non-modifiable and non-persistent.
💡 The data location for parameters of external functions must becalldata
.
Array
There are 2 kinds of arrays: fixed-size arrays and dynamic-size arrays.
The way to declare them is different. For example:
uint[3] foo
is an array that will always containing 3 unsigned integersstring[] bar
is an array of strings of dynamic size
Either way, note that all items of an array must share the same type!
You can create arrays of any type, including array
, which gives you an array of arrays. For instance, uint[][5] myArray
is a dynamic array of arrays of 5 uint (each). 🤯
Arrays indices start at 0 so you access the third uint
in the second dynamic arrays with myArray[1][2]
.
⚠️ Depending on the version of Solidity you're using, you might need to add pragma experimental ABIEncoderV2;
at the top of your file to enable that.
Special arrays
bytes
and string
are types used for variables with arbitrary-length of raw byte data and arbitrary-length of string (UTF-8) data respectively. However, if the length can be limited to a certain number of bytes it is better to use a value type like bytes32
instead of bytes
.
Storage vs memory arrays
You can create an array stored in memory with:
function f(uint len) public pure {
uint[] memory a = new uint[](len); // length is dynamically set
return a.length;
}
However its size can't change from the one provided and you can't push
an element in it.
Array literals
string memory AAVE = 'AAVE';
string[3] memory bag = ['SNX', AAVE, 'UNI'];
In this example ['SNX', AAVE, 'UNI'] is an array literal.
Array literals are memory arrays of static size. Their elements, if not of the same type, should at least be convertible to the same type.
💡You can use an array slice to get only a part of an array:
- bar[2:]
returns a slice from the 3rd element to the last
- bar[:3]
returns a slice from the 1st element to the 3rd
- bar[1:3]
returns a slice from the 2nd element to the 3rd
Struct
Structs are great for grouping variables together. They somewhat resemble objects in JavaScript. Here's how to declare one:
struct Proposal {
address owner;
string name;
string description;
uint approvals;
}
and here's how to create an instance of our struct:
Proposal memory myProposal = Proposal({owner: msg.sender, name: "Proposal 1", description: "foo", approvals: 0});
💡 It is not possible for a struct to declare a member as a struct of its own type. However, it can contain another struct type, arrays or mappings.
struct OtherStruct {
uint n;
}
struct MyStruct {
uint[] myArray; // ✅ OK
mapping(address => bool) approvals; // ✅ OK
OtherStruct bar; // ✅ OK
MyStruct foo; // ❌ NOT OK
}
Mapping
Mappings are like hash tables than can be declared with mapping(_keyType => _ValueType) mappingVariableName
. For example in mapping(address => bool) approvers
all keys have the same address type and all values are of boolean type. This is a major difference with Structs
which contain key-value pairs of different types.
There are some good things to know and a few gotchas about mappings:
- Mappings can be nested!
- The value type can be any type (even Contract type).
- All key-value pairs exist and are initialized with the default value according to the value type (empty string
""
for a mapping of strings,false
for booleans, etc...). - The key type cannot be a mapping, struct or array.
- Keys are not stored! So there is no way to retrieve them all, or loop through all the assigned keys (or not without a data structure on top of it anyway). Mappings are used to lookup one value with a specific key.
- Data location of a mapping can only be
storage
. As a result, mappings or arrays and structs containing a mapping cannot be used as parameters or parameters ofpublic
functions.
The more items you have in array the more costly it becomes to loop through it. If you face this problem, a much more efficient solution in terms of computation and gas can be to use a mapping instead.
🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁🏁
This post is meant to be an introduction so a lot things about conversion, operations etc has been omitted. For more details on Solidity types, please refer to the official Solidity documentation, making sure to select the version corresponding to the version pragma of your Solidity files.
Comments? Questions? Feedback? You spotted a mistake? Please hit me up on Twitter @RaphaelRoullet !