Notes on Solidity in Depth

Layout of a Solidity Source File

Version Pragma

1
pragma solidity ^0.4.11;

Importing other Source File

1
2
3
4
5
6
7
import "filename";

import * as symbolName from "filename";

import { symbol as alias, symbol2 } from "filename";

import "filename" as symbolName;

In the above, filename is always treated as a path with / as directory seperator.

All path names are treated as absolute paths unless thay start with . or ...

Remapping

For example, remap github.com/ethereum/dapp-bin/library to /usr/local/dapp-bin/library

solc

1
import "github.com/ethereum/dapp-bin/library/iterable_mapping.sol` as it_mapping;
1
solc github.com/ethereum/dapp-bin/=/usr/local/dapp-bin/ source.sol

Comments

Single Line Comments: //

Multi-line Comments: /* ... */

Natspec Comments: /// or /** ... */

Natspec should be used directly above function declarations or statements.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pragma solidity ^0.4.11;

/** @title Shape calculator */
contract shapeCaculator {
/** @dev Calculates a rectangle's surface and perimeter.
* @param w Width of the rectangle.
* @param h Height of the rectangle.
* @param s The calculated surface.
* @param p The calculated perimeter.
*/
function rectangle(uint w, uint h) returns (uint s, uint p) {
s = w * h;
p = 2 * (w + h);
}
}

Structure of Contract

Contracts in Solidity are similar to classes in object-oriented languages. Each contract can contain declarations of State Variables, Functions, Function Modifiers, Events, Struct Types and Enum Types.

State Variables

State variables are values which are permanently stored in contract storage.

1
2
3
4
5
pragma solidity ^0.4.11;

contract SimpleStorage {
uint storedData; // state variable
}

Functions

Functions are executable units of code within a contract.

1
2
3
4
5
6
7
pragma solidity ^0.4.11;

contract SimpleAuction {
function bid() payable {
// ...
}
}

Function Modifiers

Function modifiers can be used to amend the semantics of functions in a declarative way.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pragma solidity ^0.4.11;

contract Purchase {
address public seller;

modifier onlySeller () { // Modifier
require(msg.sender == seller);
_;
}

function abort() onlySeller { // Modifier usage
//...
}
}

Events

Events are convenience interfaces with the EVM logging facilities.

1
2
3
4
5
6
7
8
9
10
pragma solidity ^0.4.11;

contract SimpleAuction {
event HighestBidIncreased(address bidder, uint amount);

function bid() payable {
// ...
HighestBidIncreased(msg.sender, msg.value);
}
}

Struct Types

Structs are custom defined types that can group several variables.

1
2
3
4
5
6
7
8
9
10
pragma solidity ^0.4.11;

contract Ballot {
struct Voter {
uint weight;
bool voted;
address delegate;
uint vote;
}
}

Enum Types

1
2
3
4
5
6
7
8
9
pragma solidity ^0.4.0;

contract Purchase {
enum State {
Created,
Locked,
Inactive
}
}

Types

  • bool: true or false

    • ! logical negation

    • && logical and

    • || logical or

    • == logical equality

    • != logical inequality

  • int/uint: uint8 to uint256, int8 to int256

    • <=, >=, ==, !=, <, >

    • &, |, ^, ~, bit operators

    • +, -, *, /, %, **, <<, >>

  • address: holds a 20 bytes value(size of an Ethereum address)

    • <=, =>, ==, !=, <, >

Starting with version 0.5.0 contracts do not derive from the address type, but can still be explicitly converted to address.

Members of Address

  • balance

  • transfer, address.transfer(value), send value Wei to the address.

  • send: Send is the low-level counterpart of transfer. If the execution fails, the current contract will not stop with an exception, but send will return false

There are some dangers in using send: The transfer fails if the call stack depth is 1024 and it also fails if the recipient runs out of gas. So in order to make safe Ether transfers, always check the return value of send, use transfer or even better.

  • call, callcode, and delegatecall

Furthermore, to interface with contracts that do not adhere to the ABI, the function call is provided which takes an arbitrary number of arguments of any type. The arguments are padding to 32 bytes and concatenated. One exception is the case where the first argument is encoded to exactly four bytes. In this case, it is not padding to allow the use of function signatures here.

1
2
3
address nameReg = '0x72ba7d8e73fe8eb666ea66babc8116a41bfb10e2';
nameReg.call('register', 'MyName');
nameReg.call(bytes4(keccak256("fun(uint256))), );

call returns a boolean indicating whether the invoked function terminated (true) or caused an EVM exception (false). It is not possible to access the actual data returned.

It is possible to adjust the supplied gas with the .gas() modifier:

1
nameReg.call.gas(1000000000)('register', 'myName');

and supplied Ether value can be controlled too:

1
nameReg.call.value(1 ether)('regiter', 'myName');

Chained

1
nameReg.call.value(1 ether).gas(100000000)('register', 'myName');

In a similar way, the function delegatecall can be used: the difference is that only the code of the given address is used, all other aspects(storage, balance, …) are taken from the current contract. The purpose of delegatecall is to use library code which is stored in another contract. The user has to ensure that the layout of storage in both contracts is suitable for delegatecall to be used. Prior to homestead, only a limited variant called callcode was available that did not provide access to the original msg.sender and msg.value.

All three functions call, delegatecall, and callcode are very low-level functions and should only be used as a last resort as they break the type-safety of Solidity.

The .gas() option is available on all three methods, while the .value() option is not supported for delegatecall.

All Contracts inherit the members of address, so it is possible to query the balance of the current contract using this.balance.

The use of callcode is discouraged and will be removed in the future.

Fixed-size byte array

  • .length yields the fixed length of the byte array(read-only)

It is possible to use an array of bytes as byte[], but it is wasting a lot of space, 21 bytes every element, to be exact, when passing in calls. It is better to use bytes.

Dynamically-sized byte array

bytes: dynamically-sized array, see Array, not a value-type.

string: dynamically-sized UTF-8-encoded string, see Array, not a value-type.

Address Liberals

Hexadecimal literals that pass the address checksum test, for example 0xdCad3a6d3569DF655070DEd06cb7A1b2Ccd1D3AF are of address type. Hexadecimal literals that are between 39 and 41 digits long and do not pass the checksum test produce a warning and are treated as regular rational number literals

Rational and Integer Literals

Integer literal are formed form a sequence of numbers in the range 0-9. They are interpreted as decimals, for example, 69 means sixty nine. Octal literals do not exist in Solidity and leading zeros are invalid.

Decimal fraction literals are formed by a . with at least one number on one side(1., .2, 1.3)

Scientific notation is also supported, where the base can have fractions, white the exponent cannot(2e10, -2e10, 2e-10, 2.5e10)

Number literal expressions retains arbitrary precision until they are converted to a non-literal type.

For example (2**800 + 1) - 2**800 result in the constant 1, .5 * 8 results in the integer 4

Number literal expressions are converted into a non-literal type as soon as they are used with non-literal expressions. Even though we know that the value of the expression assigned to b in the following example evaluates to an integer, but the partial expression 2.5 + a does not type check so the code does not compile.

1
2
uint128 a = 1;
uint128 b = 2.5 + a + 0.5;

String Literals

String literals are written with either double or sinlge-quotes. They do not imply trailing zeros as in C, "foo" represents three bytes not four.

String Literal supports escape characters, such as \n, \xNN and \uNNNN.

Hexadecimal Literals

hex"001122FF"

Enums

Enums are one way to create a user-defined type in Solidity. They are explicitly convertable to and from all integer types but implicit conversion is not allowed. The explicit conversions check the value ranges at runtime, and a failure causes an exception.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pragma solidity ^0.4.11;

contract test {
enum ActionChoices {
GoLeft,
GoRight,
GoStraight,
GoStill
}

ActionChoices choice;
ActionChoices constant defaultChoice = ActionChoices.GoStraight;

function setGoStraight() {
choice = ActionChoices.GoStraight;
}

function getChoice() returns (ActionChoices) {
return choice;
}
function getDefaultChoice() returns (uint) {
return uint(defaultChoice);
}
}

Function Types

Function types are notated as follows:

1
function (<parameter types>) {internal|external} [pure|constant|view|payable] [returns (<return types>)]

In contrast to the parameter types, the return types cannot be empty - if the function type should not return anything, the whole returns (<return type>) part has to be omitted.

Public (or external) functions have a special member called selector, which returns the ABI function selector:

Reference Types

Complex types, i.e. types which do not always fit into 256 bits have to be handled more carefully than than the value-types we have already seen. Since copying them can be quite expensive, we have to think about whether we want them to be store in memory(which is not persisting) or storage(where the state variables are held).

Data location

Every complex type, i.e. arrays and structs has an additional annotation, the “data location” about where it is stored in memory or in storage. Depending on the context, there is always a default, but it can be overrideen by appeding either storage or memory to the type. The default for function parameters(including return parameters) is memory the default for local variable is storage and the location is forced to storage for state variables

There is also a third data location, calldata, which is a non-modificable, non-persistent area where function arguments are stored. Function parameter(not return parameters) of external functions are forced to calldata and behave mostly like memory.

Data locations are imortant because they change how assignment behave: assignments between storage and memory and also to a state variable always create an independent copy. Assignments to local storage variables only assign a reference though, and this reference always points to the state variable even if the latter is changed in the meantime. On the other hand, assignments from a memory stoted reference type to another memory-stored reference type do not create a copy.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pragma solidity ^0.4.11;

contract C {
uint[] x; // the data location of x is storage

// the data location of memoryArray is memory
function f(uint[] memoryArray) {
x = memoryArray; // works, copy the whole array to storage
var y = x; // works, assigns a pointer, data location of y is storage
y[7]; // fine, returns the 8th element
y.length = 2; // fine, modifies x through y
delete x; // fine, clears the array, also modifies y
// the following does not work; it would need to create a new temporary / unamed array in storage, but storage is 'statically' allocated
// y = memoryArray;
// This does not work either, since it would 'reset' the pointer, but there is not sensible location it could point to.
// delete y;
}
}
Summary
  • Forced data location:

    • parameters(not return) of external functions: calldata

    • state variables: storage

  • Default data location:

    • parameters(also return) of functions: memory

    • all other local variables: storage

Arrays

An array of fixed size k and element type T is written as T[k]

An array of dynamic size as T[]

An array of 5 dynamic arrays of uint is uint[][5]. (note that the notation is reversed when compared to some other language). To access the second uint in the third dynamic array, you use x[2][1]

Variables of type bytes and string are special arrays. A bytes is similar to byte[], but it is packed tightly in calldata. string is equal to bytes but does not allow length or index access(for now).

So bytes should always be preferred over byte[] because it is cheaper.

Structs

Solidity provides a way to define new types in the form of structs, which is shown in the following example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pragma solidity ^0.4.11;

contract CrowdFunding {
struct Funder {
address addr;
uint amount;
}

struct Campaign {
address beneficiary;
uint fundingGoal;
uint numFunders;
uint amount;
mapping(uint => Funder) funders;
}
}

It is not possible for a struct to contain a member of its own type, although the struct itself can be the value type of a mapping member. This restriction is necessary, as the size of the struct has to be finite.

Mappings

Mapping types are declared as mapping(_keyType => _valueType). Here _keyType can be almost any type except for a mapping, a dynamically sized array, a contract, an enum, and a struct. _valueType can actually be any type, including mappings.

Mappings can be seen as hash tables which are virtually initialized such that every possible key exists and is mapped to a value whose byte-representation is all zeros: a type’s default value.

Mappings are only allowed for state variables( or as storage reference types in internal function).

It is possible to mark mapping public and have Solidity create a getter. The _keyType will become a required parameter for the getter and it will return _valueType.

Mappings are not iterable, but it is possible to implement a data structure on top of them.

Operators Involving LValues

LValue, i.e. a variable or something that can be assigned to.

delete

delete a assigns the initial value for the type to a. I.e., for integers, it is equivalent to a = 0.

delete has no effect on whole mappints. So if you delete a struct, it will reset all members that are not mappings and also recurse into the memebers unless they are mappings. However, individual keys and what they map to can be deleted.

It’s really important to note that delete a really behaves like an assignment to a, it stores a new object in a.

Conversions between Elementary Types.

Implicit Conversions

In general, an implicit conversion between value-types is possible if it makes sense semantically and no information is lost: uint8 is convertible to uint16, but uint16 is not convertible to uint8. And int8 is not convertible to uint256(because uint256 cannot hold hold e.g. -1).

Explicit Conversions

If the compiler does not allow implicit conversion but you know what you are doing, an explicit type conversion is sometimes possible.

1
2
int8 y = -3;
uint x = uint(y);

Type Deduction

For convenience, it is not always necessary to explicitly specify the type of a variable, the compiler automatically infers it from the type of the first expression that is assigned to the variable:

1
2
uint24 x = 0x123;
var y = x;

Units and Globally Available Variables

Ether Units

A literal number can take a suffix of wei, finney, szabo, or ether to convert between the subdenominations of Ether, where Ether currency numbers without a postfix are assumed to be Wei.

2 ether === 2000 finney evaluates to true

Time Units

Suffixes like seconds, minutes, hours, days, weeks and years after literal numbers can be used to convert between units of time where seconds are the base unit.

Take care if you perform calendar calculations using these units, because not every year equals 365 days and not even every day has 24 hours because of leap seconds. Due to the fact that leap seconds cannot be predicted, an exact calendar library has to be updated by an external oracle.

1
2
3
4
5
function f(unit start, unit dayAfter) {
if (now >= start + dayAfter + 1 days) {
// ...
}
}

Other Global Variables and Functions:

Post

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×