Soldity Crash Course
This is a very concise introduction into Solidity, the programming language of the Ethereum blockchain.
We want to build a decentralized
application and to make our application tamperproof, we want to store information
on the blockchain.
To achieve this specific task, we need to
provide the blockchain with a specific set of rules. This set of rules will be
defined in our smart contract. It
will define functions we will later call by sending transaction to the blockchain.
It regulates the way users can access and manipulate the sensitive data we want
to store.
Ethereum supports two types of accounts.
The ones controlled by a user holding a private key and the ones controlled by
code, the smart contract. It
essentially becomes an autonomous agent in the network that will be able to
receive and send funds and manipulate its own storage.
Because smart
contracts serve a unique purpose, it makes sense that they are written in their
own unique language.
Solidity is a high-level language for
writing smart contracts. Its syntax
is very similar to JavaScript, but it is statically typed. This means that data
types must be set and are enforced at compile-time. Like other object oriented
languages, it supports inheritance, libraries and user-defined objects.
Please
keep in mind that the information covered in this chapter is not a complete
representation of everything that is possible in Solidity. Rather, we will look
at the most important parts, moving from intuitive to the more complicated
ones.
General Structure
Files written in Solidity are identified by
the filename extension “.sol”. The first line always contains the version of
Solidity used in the file.
1. pragma solidity ^ 0.4.6;
pragma stands for “pragmatic
information” and indicates that the text that follows is meaningful to the
compiler.
After that, a sol-file can contain a
number of contracts, but most of the time it only contains one, just like most
files when using object-oriented programming languages will only contain one
class.
Contracts are defined as follows:
1. contract MyContract {
2.
3. }
Our new contract
“MyContract” is still very empty so let’s fill it up
with some functions.
Solidity makes a very important distinction
between two types of functions. The ones that alter the state of the blockchain
(i.e. by manipulating the contract’s
storage) and the ones that don’t.
The latter are marked with the Identifier
constant and do not cost any Ether
to execute since we are only reading from the existing state of the blockchain
from our local node.
Both types follow the same principles and are
defined as follows. The red parts are optional.
function name(type1 variable1, type2 variable2) identifiers returns(type1, type2) {}
1. contract MyContract {
2. function put(int x) {
3. //do something
4. }
5. }
We just created a
function “put” that expects an Integer “x” as an input variable.
In order to save x, we are going to create a variable to store it in. Following
the principles of encapsulation, this variable will be defined outside the function
but inside the contract. We will be able to access it from everywhere inside
the contract.
1. contract MyContract {
2. int save;
3.
4. function put(int x) {
5. save = x;
6. }
7. }
Calling this
function will now put the value “x” into our variable “save”. It is now saved
in the storage associated with our smart contract and is therefore engraved
in the blockchain. The only way to manipulate it, is through our very own
contract. But first, we want to be able to read the value we just saved. Again,
similar to other object-oriented languages, we can either make our Integer
public (int public save;) or write a
“get”-function. We will do the latter.
1. contract MyContract {
2. int save;
3.
4. function put(int x) {
5. save = x;
6. }
7.
8. function get() constant returns(int) {
9. return save;
10. }
11. }
Our function
“get” does not take any input variables. It is marked with the Identifier constant, which indicates that we are dealing with a function that
does not alter the contract’s storage
and thereby the state of the blockchain. As mentioned before, calling this
function will not cost any Ether.
Of course, integers aren’t the only type of data we can use
in Solidity.
Simple Types
What types of variables can we use in
Solidity and how are they defined?
Booleans are defined as bool and can be either true orfalse. Operators are same as
in JavaScript (!, &&, ||, ==, != )
Integers are defined as int for signed and uint for unsigned integers. int distributes
its range around 0, while uint only holds
positive values. If your variable does not need negative values, using uint will offer double the amount of range. The operators are also the
same
(i<, <=, ==, !=, >=, > ).
Strings
can be used in Solidity as well. It is important to
understand that a string is just a dynamically-sized byte array, storing UTF-8-encoded
characters. Because handling dynamically-sized arrays is expensive and will
consume a lot of gas, it is always worth a thought if a string is really necessary.
Maybe a fixed-size byte representation will also do the trick. These range from
bytes1 to bytes32.
Addresses
are a type that is unique to Solidity. An address is a 20 byte
value that is fitted to hold an Ethereum address. The special thing about the
address type are its two member functions.
With address.balance we can retrieve the addresse’s Ether balance in Wei. Because Ether can be
split into very small fractions, units exist to simplify its handling. For now
we only need to know about the smallest unit, Wei.
1 000 000 000 000 000 000 wei == 1 ether
These units can be used directly in Solidity as
we will see later on. Most functions that deal with Ether will take arguments in Wei,
just like address.send().
address.send() can be used to
let the contract send Ether to that
address.
1. address a = 0xB494124a56878E68d10cc72c7f0d5aB98bbF5409;
2. bool success = a.send(10);
We just sent 10 Wei to the
address we defined.
There is one very important thing to remember here. send() returns a bool indicating if
the transfer was successful. We should always check for that, before we move on
with our function. This is why it is often used inside and if-statement like
this:
1. if (!x.send(10)) return;
This way, we can assure that our process will interrupt and roll back in case the transfer fails. A fail can actually very easily be achieved by a malevolent party by artificially overloading the call stack of the function call, because the call stack depth is fixed at 1024. A transaction can also fail if it runs out of gas.
It is recommended
to use the address.transfer() function instead. It will make
the contract call stop with an exception if the transfer fails.
Or even better: we avoid actively sending money completely
by implementing patterns where the recipient withdraws the money.
A more advanced example
Contracts are not
only used to handle the storing and manipulation of data, but also funds. Let’s
say we want to write a contract that stores the address of every user that
contributes 1 Ether to it. In order
to do that, we will need to create an array of addresses. We will make it
public so we can read it directly when testing.
1. address[] public contributors;
Now we need a
function that will create the entry. Some new elements are used in this
function.
1. payable – Not every function in your contract is able to receive Ether. Remember that functions are
called by sending a transaction to the contract’s address. The data field of the transaction defines
which function will be called and also holds the arguments. The value field, just like in a transaction
from person to person, indicates the amount of Ether that is sent with the transaction. Now, sending Ether to a contract can be dangerous
because, it being an autonomous agent, there may be no way of retrieving it
afterwards. In order to prevent Ether accidently
being sent to our contract, we have to specifically mark those functions that
accept Ether as payable.
2. msg.value – Solidity features some blockchain-specific variables, which are
globally available. block provides us with information about the current block like block.number,block.timestamp or block.coinbase.
now will also return
the timestamp of the current block.
tx.gasprice and tx.origin deliver information about the transaction that initiated the call.
msg is the most interesting for our purposes, as it can give us the
address of the sender of the message (msg.sender), as well as
well as the amount of Wei that was
sent with it (msg.value).
3. array.push() – When we want to add a new
entry to a dynamically-sized array like “contributors”, the push()-function comes in
handy. As an argument, we will provide the object when want to push on top of
the exisiting array.
Deleting an entry from an array can be achieved using delete array[index]. This will
delete the entry corresponding to that index, but will leave it empty. To
rearrange the array manually, we would need to write our own loop.
This is our contract, using all the new
features we just learned.
1. contract MyContract {
2. address[] public contributors;
3.
4. function contribute() payable {
5. if (msg.value == 1 ether) {
6. contributors.push(msg.sender);
7. }
8. }
9. }
If we call the
function “contribute” and send exactly 1 Ether
with the transaction, our address will be pushed into the “contributors” array to record the contribution we made. Using indices, we could read
addresses from the array (contr[index]) and even call it directly from our application with the web3.js API (MyContract.contr(index)) because it is public. We will learn more on how to do
that later.
Structs and Mappings
Say we want our
contract to remember more than just the contributors address. Maybe one can now
contribute more than 1 Ether and we
want to record the amount and also the real name of the contributor. To achieve
that, we can create a struct.
1. struct Contributor {
2. address a;
3. uint amount;
4. string name;
5. }
We can then
simply use the newly created structure and make an array out of it.
1. Contributor[] contributors;
Filling this array would look like this.
1. function contribute(string name) payable {
2. if (msg.value >= 1 ether) {
3. contributors.push(Contributor(msg.sender, msg.value, name));
4. }
5. }
Solidity makes it easy to create new instances of our struct. We have to follow the
right order of attributes and use the syntax struct{type1 attribute1,type2 attribute2, …} to create it. We then push it directly into our array of structs
created earlier.
It would be very convenient to be able to receive this whole
array from the smart contract.
Unfortunately, this is not possible yet!
If we think of possible use cases like querying for a specific address, we
would have to get every single entry in separate calls from the blockchain. We
could also implement the search algorithm in the smart contract. This could look like this:
1. function getContrOf(address _a) constant returns(uint) {
2. for (uint i = 0; i < contributors.length; i++) {
3. if (contributors[i].a == _a) {
4. return contributors[i].amount;
5. }
6. }
7. return 0;
8. }
Notice that this function is also constant. The for-loop is
familiar from jacaScript and we see that we can use
the .length attribute
of our array. We are addressing the different parts of it using the array[index].attribute syntax
known from other object-oriented languages. If we find an address corresponding
to the one given to us in the function, contributors[i].amount is returned. If we don’t find anything, we return 0.
Another elegant way to solve our problem would be to use a mapping.
It is essentially a key-value store. Here, we would be able to make the address
point directly to the rest of our data. Creating a mapping is very simple.
1. mapping (address => Contributor) contributors;
We have created a mapping where an address will act as a key and point to our self-created struct object as a value. We
can now directly look for an entry corresponding to any address.
Filling this mapping would be done as
follows.
1. function contribute(string name) {
2. if (msg.value >= 1 ether) {
3. contrs[msg.sender] = Contributor(msg.sender, msg.value, name);
4. }
5. }
In this case, we assign
the “Contributor” instance to the field in our mapping with the key being the
address. We actually could have gotten rid of the address attribute in the
struct, but left it in for simplicity. Retreiving a
specific contribution is now fairly easy.
1. function getContrOf(address a) constant returns(uint) {
2. return contrs[a].amount;
3. }
The advantage of the mapping is that we do not need
to look through our data manually. The downside is we have no way of knowing
how many entries there are and can therefore not retrieve all data, even with
repetitive calls as mentioned before when talking about arrays.
A way around this trade-off would be to store all the keys
of our mapping in an extra array.
Events and Oracles
Having learned about the most important
capabilities of Solidity, it is also necessary to understand some fundamental
limitations of smart contracts before developing a ÐApp.
One
of the first ideas that come to mind when learning about blockchain is an
application where the user can bet on the weather the next day or the result of
a soccer game. The problem with these kind of applications is that they require
information from outside the blockchain. Say, a contract would consult a
standardized web service about the soccer result and use this information to make
payouts. Every node would process the transaction, execute the code and call
the web service. The web service can only answer to one call at a time. This is
fine as long as the response for every call will be the same. But what if for
some reason the web service shuts down halfway through the requests. Half the
nodes would get an invalid result, while the other half executed the contract
using the received results. This sort of un-deterministic behaviour
would endanger the blockchain and is therefore not possible. Instead, so called
oracles can be used, which would
reliably publish information into the blockchain for it to be used by smart
contracts.
Another problem would be calling an external API from a smart contract. Naturally, the API should not be called individually by every node as that would cause the called function to be executed multiple times and nodes would get different results depending on whether the call was successful or not. Here, the solution would be using EventLogs. Smart contracts can throw Events which applications can listen for. This means, instead of calling the API, a service would monitor the blockchain for a specific Event and then execute the call itself. Both these solutions require an outside party that needs to be trusted, so the trust created by the blockchain does not apply here. In the end, interactions with the blockchain are limited to simple write (oracle) and read (EventLogs) operations.
The End
I hope this tutorial was helpful to get you started with Solidity and give you a better and deeper understanding of Ethereum in general.
As we only covered some parts of Solidity, I encourage you to have a look at the full documentation at: https://solidity.readthedocs.io/en/develop/solidity-in-depth.html
Thanks for reading
SIMON DOS
Comments
Post a Comment