Lesson 1
Unsigned Integers: uint
The uint
data type is an unsigned integer, meaning its value must be non-negative. There’s also an int
data type for signed integers.
Note: In Solidity,
uint
is actually an alias foruint256
, a 256-bit unsigned integer. You can declare uints with less bits —uint8
,uint16
,uint32
, etc.. But in general you want to simply useuint
except in specific cases.
Structs
Sometimes you need a more complex data type. For this, Solidity provides *structs*:
struct Person {
uint age;
string name;
}
Structs allow you to create more complicated data types that have multiple properties.
Note that we just introduced a new type,
string
. Strings are used for arbitrary-length UTF-8 data. Ex.string greeting = "Hello world!"
Creating New Structs
Remember our Person
struct in the previous example?
struct Person {
uint age;
string name;
}
Person[] public people;
Now we’re going to learn how to create new Person
s and add them to our people
array.
// create a New Person:
Person satoshi = Person(172, "Satoshi");
// Add that person to the Array:
people.push(satoshi);
We can also combine these together and do them in one line of code to keep things clean:
people.push(Person(16, "Vitalik"));
Note that array.push()
adds something to the end of the array, so the elements are in the order we added them. See the following example:
uint[] numbers;
numbers.push(5);
numbers.push(10);
numbers.push(15);
// numbers is now equal to [5, 10, 15]
Arrays
When you want a collection of something, you can use an array***. There are two types of arrays in Solidity: **fixed* arrays and *dynamic* arrays:
// Array with a fixed length of 2 elements:
uint[2] fixedArray;
// another fixed Array, can contain 5 strings:
string[5] stringArray;
// a dynamic Array - has no fixed size, can keep growing:
uint[] dynamicArray;
You can also create an array of *structs*. Using the previous chapter’s Person
struct:
Person[] people; // dynamic Array, we can keep adding to it
Remember that state variables are stored permanently in the blockchain? So creating a dynamic array of structs like this can be useful for storing structured data in your contract, kind of like a database.
Public Arrays
You can declare an array as public
, and Solidity will automatically create a *getter* method for it. The syntax looks like:
Person[] public people;
Other contracts would then be able to read from, but not write to, this array. So this is a useful pattern for storing public data in your contract.
Function Declarations
A function declaration in solidity looks like the following:
function eatHamburgers(string memory _name, uint _amount) public {
}
This is a function named eatHamburgers
that takes 2 parameters: a string
and a uint
. For now the body of the function is empty. Note that we’re specifying the function visibility as public
. We’re also providing instructions about where the _name
variable should be stored- in memory
. This is required for all reference types such as arrays, structs, mappings, and strings.
What is a reference type you ask?
Well, there are two ways in which you can pass an argument to a Solidity function:
- By value, which means that the Solidity compiler creates a new copy of the parameter’s value and passes it to your function. This allows your function to modify the value without worrying that the value of the initial parameter gets changed.
- By reference, which means that your function is called with a… reference to the original variable. Thus, if your function changes the value of the variable it receives, the value of the original variable gets changed.
Note: It’s convention (but not required) to start function parameter variable names with an underscore (
_
) in order to differentiate them from global variables. We’ll use that convention throughout our tutorial.
You would call this function like so:
eatHamburgers("vitalik", 100);
Private / Public Functions
In Solidity, functions are public
by default. This means anyone (or any other contract) can call your contract’s function and execute its code.
Obviously this isn’t always desirable, and can make your contract vulnerable to attacks. Thus it’s good practice to mark your functions as private
by default, and then only make public
the functions you want to expose to the world.
Let’s look at how to declare a private function:
uint[] numbers;
function _addToArray(uint _number) private {
numbers.push(_number);
}
This means only other functions within our contract will be able to call this function and add to the numbers
array.
As you can see, we use the keyword private
after the function name. And as with function parameters, it’s convention to start private function names with an underscore (_
).
Math Operations
Math in Solidity is pretty straightforward. The following operations are the same as in most programming languages:
- Addition:
x + y
- Subtraction:
x - y
, - Multiplication:
x * y
- Division:
x / y
- Modulus / remainder:
x % y
(for example,13 % 5
is3
, because if you divide 5 into 13, 3 is the remainder)
Solidity also supports an *exponential operator* (i.e. “x to the power of y”, x^y)
Return Values
To return a value from a function, the declaration looks like this:
string greeting = "What's up dog";
function sayHello() public returns (string memory) {
return greeting;
}
In Solidity, the function declaration contains the type of the return value (in this case string
).
Function modifiers
The above function doesn’t actually change state in Solidity — e.g. it doesn’t change any values or write anything.
So in this case we could declare it as a *view* function, meaning it’s only viewing the data but not modifying it:
function sayHello() public view returns (string memory) {
Solidity also contains *pure* functions, which means you’re not even accessing any data in the app. Consider the following:
function _multiply(uint a, uint b) private pure returns (uint) {
return a * b;
}
This function doesn’t even read from the state of the app — its return value depends only on its function parameters. So in this case we would declare the function as *pure*.
Note: It may be hard to remember when to mark functions as pure/view. Luckily the Solidity compiler is good about issuing warnings to let you know when you should use one of these modifiers.
Keccak256
Ethereum has the hash function keccak256
built in, which is a version of SHA3. A hash function basically maps an input into a random 256-bit hexadecimal number. A slight change in the input will cause a large change in the hash.
It’s useful for many purposes in Ethereum, but for right now we’re just going to use it for pseudo-random number generation.
Also important, keccak256
expects a single parameter of type bytes
. This means that we have to “pack” any parameters before calling keccak256
:
Example:
//6e91ec6b618bb462a4a6ee5aa2cb0e9cf30f7a052bb467b0ba58b8748c00d2e5
keccak256(abi.encodePacked("aaaab"));
//b1f078126895a1424524de5321b339ab00408010b7cf0e6ed451514981e58aa9
keccak256(abi.encodePacked("aaaac"));
As you can see, the returned values are totally different despite only a 1 character change in the input.
Typecasting
Sometimes you need to convert between data types. Take the following example:
uint8 a = 5;
uint b = 6;
// throws an error because a * b returns a uint, not uint8:
uint8 c = a * b;
// we have to typecast b as a uint8 to make it work:
uint8 c = a * uint8(b);
In the above, a * b
returns a uint
, but we were trying to store it as a uint8
, which could cause potential problems. By casting it as a uint8
, it works and the compiler won’t throw an error.
Events
*Events* are a way for your contract to communicate that something happened on the blockchain to your app front-end, which can be ‘listening’ for certain events and take action when they happen.
Example:
// declare the event
event IntegersAdded(uint x, uint y, uint result);
function add(uint _x, uint _y) public returns (uint) {
uint result = _x + _y;
// fire an event to let the app know the function was called:
emit IntegersAdded(_x, _y, result);
return result;
}
Your app front-end could then listen for the event. A javascript implementation would look something like:
YourContract.IntegersAdded(function(error, result) {
// do something with result
})
Lesson 2
Addresses
The Ethereum blockchain is made up of accounts***, which you can think of like bank accounts. An account has a balance of **Ether* (the currency used on the Ethereum blockchain), and you can send and receive Ether payments to other accounts, just like your bank account can wire transfer money to other bank accounts.
Each account has an address
, which you can think of like a bank account number. It’s a unique identifier that points to that account, and it looks like this:
0x0cE446255506E92DF41614C46F1d6df9Cc969183
Mappings
In Lesson 1 we looked at structs*** and **arrays*. *Mappings* are another way of storing organized data in Solidity.
Defining a mapping
looks like this:
// For a financial app, storing a uint that holds the user's account balance:
mapping (address => uint) public accountBalance;
// Or could be used to store / lookup usernames based on userId
mapping (uint => string) userIdToName;
A mapping is essentially a key-value store for storing and looking up data. In the first example, the key is an address
and the value is a uint
, and in the second example the key is a uint
and the value a string
.
msg.sender
In Solidity, there are certain global variables that are available to all functions. One of these is msg.sender
, which refers to the address
of the person (or smart contract) who called the current function.
Note: In Solidity, function execution always needs to start with an external caller. A contract will just sit on the blockchain doing nothing until someone calls one of its functions. So there will always be a
msg.sender
.
Here’s an example of using msg.sender
and updating a mapping
:
mapping (address => uint) favoriteNumber;
function setMyNumber(uint _myNumber) public {
// Update our `favoriteNumber` mapping to store `_myNumber` under `msg.sender`
favoriteNumber[msg.sender] = _myNumber;
// ^ The syntax for storing data in a mapping is just like with arrays
}
function whatIsMyNumber() public view returns (uint) {
// Retrieve the value stored in the sender's address
// Will be `0` if the sender hasn't called `setMyNumber` yet
return favoriteNumber[msg.sender];
}
In this trivial example, anyone could call setMyNumber
and store a uint
in our contract, which would be tied to their address. Then when they called whatIsMyNumber
, they would be returned the uint
that they stored.
Using msg.sender
gives you the security of the Ethereum blockchain — the only way someone can modify someone else’s data would be to steal the private key associated with their Ethereum address.
Require
require
makes it so that the function will throw an error and stop executing if some condition is not true:
function sayHiToVitalik(string memory _name) public returns (string memory) {
// Compares if _name equals "Vitalik". Throws an error and exits if not true.
// (Side note: Solidity doesn't have native string comparison, so we
// compare their keccak256 hashes to see if the strings are equal)
require(keccak256(abi.encodePacked(_name)) == keccak256(abi.encodePacked("Vitalik")));
// If it's true, proceed with the function:
return "Hi!";
}
If you call this function with sayHiToVitalik("Vitalik")
, it will return “Hi!”. If you call it with any other input, it will throw an error and not execute.
Thus require
is quite useful for verifying certain conditions that must be true before running a function.
Inheritance
Rather than making one extremely long contract, sometimes it makes sense to split your code logic across multiple contracts to organize the code.
One feature of Solidity that makes this more manageable is contract *inheritance*:
contract Doge {
function catchphrase() public returns (string memory) {
return "So Wow CryptoDoge";
}
}
contract BabyDoge is Doge {
function anotherCatchphrase() public returns (string memory) {
return "Such Moon BabyDoge";
}
}
BabyDoge
*inherits* from Doge
. That means if you compile and deploy BabyDoge
, it will have access to both catchphrase()
and anotherCatchphrase()
(and any other public functions we may define on Doge
).
This can be used for logical inheritance (such as with a subclass, a Cat
is an Animal
). But it can also be used simply for organizing your code by grouping similar logic together into different contracts.
Import
Our code was getting pretty long, so we split it up into multiple files to make it more manageable. This is normally how you will handle long codebases in your Solidity projects.
When you have multiple files and you want to import one file into another, Solidity uses the import
keyword:
import "./someothercontract.sol";
contract newContract is SomeOtherContract {
}
So if we had a file named someothercontract.sol
in the same directory as this contract (that’s what the ./
means), it would get imported by the compiler.
Storage vs Memory (Data location)
In Solidity, there are two locations you can store variables — in storage
and in memory
.
Storage*** refers to variables stored permanently on the blockchain. **Memory* variables are temporary, and are erased between external function calls to your contract. Think of it like your computer’s hard disk vs RAM.
Most of the time you don’t need to use these keywords because Solidity handles them by default. State variables (variables declared outside of functions) are by default storage
and written permanently to the blockchain, while variables declared inside functions are memory
and will disappear when the function call ends.
However, there are times when you do need to use these keywords, namely when dealing with structs*** and **arrays* within functions:
contract SandwichFactory {
struct Sandwich {
string name;
string status;
}
Sandwich[] sandwiches;
function eatSandwich(uint _index) public {
// Sandwich mySandwich = sandwiches[_index];
// ^ Seems pretty straightforward, but solidity will give you a warning
// telling you that you should explicitly declare `storage` or `memory` here.
// So instead, you should declare with the `storage` keyword, like:
Sandwich storage mySandwich = sandwiches[_index];
// ...in which case `mySandwich` is a pointer to `sandwiches[_index]`
// in storage, and...
mySandwich.status = "Eaten!";
// ...this will permanently change `sandwiches[_index]` on the blockchain.
// If you just want a copy, you can use `memory`:
Sandwich memory anotherSandwich = sandwiches[_index + 1];
// ...in which case `anotherSandwich` will simply be a copy of the
// data in memory, and...
anotherSandwich.status = "Eaten!";
// ...will just modify the temporary variable and have no effect
// on `sandwiches[_index + 1]`. But you can do this:
sandwiches[_index + 1] = anotherSandwich;
// ...if you want to copy the changes back into blockchain storage.
}
}
Don’t worry if you don’t fully understand when to use which one yet — throughout this tutorial we’ll tell you when to use storage
and when to use memory
, and the Solidity compiler will also give you warnings to let you know when you should be using one of these keywords.
Internal and External
In addition to public
and private
, Solidity has two more types of visibility for functions: internal
and external
.
internal
is the same as private
, except that it’s also accessible to contracts that inherit from this contract.
external
is similar to public
, except that these functions can ONLY be called outside the contract — they can’t be called by other functions inside that contract. We’ll talk about why you might want to use external
vs public
later.
For declaring internal
or external
functions, the syntax is the same as private
and public
:
contract Sandwich {
uint private sandwichesEaten = 0;
function eat() internal {
sandwichesEaten++;
}
}
contract BLT is Sandwich {
uint private baconSandwichesEaten = 0;
function eatWithBacon() public returns (string memory) {
baconSandwichesEaten++;
// We can call this here because it's internal
eat();
}
}
Interface
For our contract to talk to another contract on the blockchain that we don’t own, first we need to define an *interface*.
Let’s look at a simple example. Say there was a contract on the blockchain that looked like this:
contract LuckyNumber {
mapping(address => uint) numbers;
function setNum(uint _num) public {
numbers[msg.sender] = _num;
}
function getNum(address _myAddress) public view returns (uint) {
return numbers[_myAddress];
}
}
This would be a simple contract where anyone could store their lucky number, and it will be associated with their Ethereum address. Then anyone else could look up that person’s lucky number using their address.
Now let’s say we had an external contract that wanted to read the data in this contract using the getNum
function.
First we’d have to define an *interface* of the LuckyNumber
contract:
contract NumberInterface {
function getNum(address _myAddress) public view returns (uint);
}
Notice that this looks like defining a contract, with a few differences. For one, we’re only declaring the functions we want to interact with — in this case getNum
— and we don’t mention any of the other functions or state variables.
Secondly, we’re not defining the function bodies. Instead of curly braces ({
and }
), we’re simply ending the function declaration with a semi-colon (;
).
So it kind of looks like a contract skeleton. This is how the compiler knows it’s an interface.
By including this interface in our dapp’s code our contract knows what the other contract’s functions look like, how to call them, and what sort of response to expect.
We can use interface in a contract as follows:
contract MyContract {
address NumberInterfaceAddress = 0xab38...
// ^ The address of the FavoriteNumber contract on Ethereum
NumberInterface numberContract = NumberInterface(NumberInterfaceAddress);
// Now `numberContract` is pointing to the other contract
function someFunction() public {
// Now we can call `getNum` from that contract:
uint num = numberContract.getNum(msg.sender);
// ...and do something with `num` here
}
}
In this way, your contract can interact with any other contract on the Ethereum blockchain, as long they expose those functions as public
or external
.
Handling Multiple Return Values
This getKitty
function is the first example we’ve seen that returns multiple values. Let’s look at how to handle them:
function multipleReturns() internal returns(uint a, uint b, uint c) {
return (1, 2, 3);
}
function processMultipleReturns() external {
uint a;
uint b;
uint c;
// This is how you do multiple assignment:
(a, b, c) = multipleReturns();
}
// Or if we only cared about one of the values:
function getLastReturnValue() external {
uint c;
// We can just leave the other fields blank:
(,,c) = multipleReturns();
}
If statements
If statements in Solidity look just like javascript:
function eatBLT(string memory sandwich) public {
// Remember with strings, we have to compare their keccak256 hashes
// to check equality
if (keccak256(abi.encodePacked(sandwich)) == keccak256(abi.encodePacked("BLT"))) {
eat();
}
}
Lesson 3
Ownable Contracts
Contracts Ownable
— meaning they have an owner (you) who has special privileges.
Below is the Ownable
contract taken from the *OpenZeppelin* Solidity library. OpenZeppelin is a library of secure and community-vetted smart contracts that you can use in your own DApps.
Give the contract below a read-through. You’re going to see a few things we haven’t learned yet, but don’t worry, we’ll talk about them afterward.
/**
* @title Ownable
* @dev The Ownable contract has an owner address, and provides basic authorization control
* functions, this simplifies the implementation of "user permissions".
*/
contract Ownable {
address private _owner;
event OwnershipTransferred(
address indexed previousOwner,
address indexed newOwner
);
/**
* @dev The Ownable constructor sets the original `owner` of the contract to the sender
* account.
*/
constructor() internal {
_owner = msg.sender;
emit OwnershipTransferred(address(0), _owner);
}
/**
* @return the address of the owner.
*/
function owner() public view returns(address) {
return _owner;
}
/**
* @dev Throws if called by any account other than the owner.
*/
modifier onlyOwner() {
require(isOwner());
_;
}
/**
* @return true if `msg.sender` is the owner of the contract.
*/
function isOwner() public view returns(bool) {
return msg.sender == _owner;
}
/**
* @dev Allows the current owner to relinquish control of the contract.
* @notice Renouncing to ownership will leave the contract without an owner.
* It will not be possible to call the functions with the `onlyOwner`
* modifier anymore.
*/
function renounceOwnership() public onlyOwner {
emit OwnershipTransferred(_owner, address(0));
_owner = address(0);
}
/**
* @dev Allows the current owner to transfer control of the contract to a newOwner.
* @param newOwner The address to transfer ownership to.
*/
function transferOwnership(address newOwner) public onlyOwner {
_transferOwnership(newOwner);
}
/**
* @dev Transfers control of the contract to a newOwner.
* @param newOwner The address to transfer ownership to.
*/
function _transferOwnership(address newOwner) internal {
require(newOwner != address(0));
emit OwnershipTransferred(_owner, newOwner);
_owner = newOwner;
}
}
A few new things here we haven’t seen before:
- Constructors:
function Ownable()
is a *constructor*, which is an optional special function that has the same name as the contract. It will get executed only one time, when the contract is first created. - Function Modifiers:
modifier onlyOwner()
. Modifiers are kind of half-functions that are used to modify other functions, usually to check some requirements prior to execution. In this case,onlyOwner
can be used to limit access so only the owner of the contract can run this function. We’ll talk more about function modifiers in the next chapter, and what that weird_;
does. indexed
keyword: don’t worry about this one, we don’t need it yet.
So the Ownable
contract basically does the following:
- When a contract is created, its constructor sets the
owner
tomsg.sender
(the person who deployed it) - It adds an
onlyOwner
modifier, which can restrict access to certain functions to only theowner
- It allows you to transfer the contract to a new
owner
onlyOwner
is such a common requirement for contracts that most Solidity DApps start with a copy/paste of this Ownable
contract, and then their first contract inherits from it.
Function Modifiers
A function modifier looks just like a function, but uses the keyword modifier
instead of the keyword function
. And it can’t be called directly like a function can — instead we can attach the modifier’s name at the end of a function definition to change that function’s behavior.
Let’s take a closer look by examining onlyOwner
:
Notice the onlyOwner
modifier on the renounceOwnership
function. When you call renounceOwnership
, the code inside onlyOwner
executes first. Then when it hits the _;
statement in onlyOwner
, it goes back and executes the code inside renounceOwnership
.
So while there are other ways you can use modifiers, one of the most common use-cases is to add a quick require
check before a function executes.
In the case of onlyOwner
, adding this modifier to a function makes it so only the owner of the contract (you, if you deployed it) can call that function.
Function modifiers with arguments
Previously we looked at the simple example of onlyOwner
. But function modifiers can also take arguments. For example:
// A mapping to store a user's age:
mapping (uint => uint) public age;
// Modifier that requires this user to be older than a certain age:
modifier olderThan(uint _age, uint _userId) {
require(age[_userId] >= _age);
_;
}
// Must be older than 16 to drive a car (in the US, at least).
// We can call the `olderThan` modifier with arguments like so:
function driveCar(uint _userId) public olderThan(16, _userId) {
// Some function logic
}
You can see here that the olderThan
modifier takes arguments just like a function does. And that the driveCar
function passes its arguments to the modifier.
Let’s try making our own modifier
that uses the zombie level
property to restrict access to special abilities.
Gas — the fuel Ethereum DApps run on
In Solidity, your users have to pay every time they execute a function on your DApp using a currency called *gas*. Users buy gas with Ether (the currency on Ethereum), so your users have to spend ETH in order to execute functions on your DApp.
How much gas is required to execute a function depends on how complex that function’s logic is. Each individual operation has a gas cost*** based roughly on how much computing resources will be required to perform that operation (e.g. writing to storage is much more expensive than adding two integers). The total **gas cost* of your function is the sum of the gas costs of all its individual operations.
Because running functions costs real money for your users, code optimization is much more important in Ethereum than in other programming languages. If your code is sloppy, your users are going to have to pay a premium to execute your functions — and this could add up to millions of dollars in unnecessary fees across thousands of users.
Struct packing to save gas
In Lesson 1, we mentioned that there are other types of uint
s: uint8
, uint16
, uint32
, etc.
Normally there’s no benefit to using these sub-types because Solidity reserves 256 bits of storage regardless of the uint
size. For example, using uint8
instead of uint
(uint256
) won’t save you any gas.
But there’s an exception to this: inside struct
s.
If you have multiple uint
s inside a struct, using a smaller-sized uint
when possible will allow Solidity to pack these variables together to take up less storage. For example:
struct NormalStruct {
uint a;
uint b;
uint c;
}
struct MiniMe {
uint32 a;
uint32 b;
uint c;
}
// `mini` will cost less gas than `normal` because of struct packing
NormalStruct normal = NormalStruct(10, 20, 30);
MiniMe mini = MiniMe(10, 20, 30);
For this reason, inside a struct you’ll want to use the smallest integer sub-types you can get away with.
You’ll also want to cluster identical data types together (i.e. put them next to each other in the struct) so that Solidity can minimize the required storage space. For example, a struct with fields uint c; uint32 a; uint32 b;
will cost less gas than a struct with fields uint32 a; uint c; uint32 b;
because the uint32
fields are clustered together.
Time units
Solidity provides some native units for dealing with time.
The variable now
will return the current unix timestamp of the latest block (the number of seconds that have passed since January 1st 1970). The unix time as I write this is 1515527488
.
Note: Unix time is traditionally stored in a 32-bit number. This will lead to the “Year 2038” problem, when 32-bit unix timestamps will overflow and break a lot of legacy systems. So if we wanted our DApp to keep running 20 years from now, we could use a 64-bit number instead — but our users would have to spend more gas to use our DApp in the meantime. Design decisions!
Solidity also contains the time units seconds
, minutes
, hours
, days
, weeks
and years
. These will convert to a uint
of the number of seconds in that length of time. So 1 minutes
is 60
, 1 hours
is 3600
(60 seconds x 60 minutes), 1 days
is 86400
(24 hours x 60 minutes x 60 seconds), etc.
Here’s an example of how these time units can be useful:
uint lastUpdated;
// Set `lastUpdated` to `now`
function updateTimestamp() public {
lastUpdated = now;
}
// Will return `true` if 5 minutes have passed since `updateTimestamp` was
// called, `false` if 5 minutes have not passed
function fiveMinutesHavePassed() public view returns (bool) {
return (now >= (lastUpdated + 5 minutes));
}
View functions don’t cost gas
view
functions don’t cost any gas when they’re called externally by a user.
This is because view
functions don’t actually change anything on the blockchain – they only read the data. So marking a function with view
tells web3.js
that it only needs to query your local Ethereum node to run the function, and it doesn’t actually have to create a transaction on the blockchain (which would need to be run on every single node, and cost gas).
We’ll cover setting up web3.js with your own node later. But for now the big takeaway is that you can optimize your DApp’s gas usage for your users by using read-only external view
functions wherever possible.
Note: If a
view
function is called internally from another function in the same contract that is not aview
function, it will still cost gas. This is because the other function creates a transaction on Ethereum, and will still need to be verified from every node. Soview
functions are only free when they’re called externally.
Declaring arrays in memory
You can use the memory
keyword with arrays to create a new array inside a function without needing to write anything to storage. The array will only exist until the end of the function call, and this is a lot cheaper gas-wise than updating an array in storage
— free if it’s a view
function called externally.
Here’s how to declare an array in memory:
function getArray() external pure returns(uint[] memory) {
// Instantiate a new array in memory with a length of 3
uint[] memory values = new uint[](3);
// Put some values to it
values[0] = 1;
values[1] = 2;
values[2] = 3;
return values;
}
This is a trivial example just to show you the syntax, but in the next chapter we’ll look at combining this with for
loops for real use-cases.
Note: memory arrays must be created with a length argument (in this example,
3
). They currently cannot be resized like storage arrays can witharray.push()
, although this may be changed in a future version of Solidity.
Using for
loops
The syntax of for
loops in Solidity is similar to JavaScript.
Let’s look at an example where we want to make an array of even numbers:
function getEvens() pure external returns(uint[] memory) {
uint[] memory evens = new uint[](5);
// Keep track of the index in the new array:
uint counter = 0;
// Iterate 1 through 10 with a for loop:
for (uint i = 1; i <= 10; i++) {
// If `i` is even...
if (i % 2 == 0) {
// Add it to our array
evens[counter] = i;
// Increment counter to the next empty index in `evens`:
counter++;
}
}
return evens;
}
This function will return an array with the contents [2, 4, 6, 8, 10]
.
Lesson 4
Payable Modifier
These modifiers can all be stacked together on a function definition as follows:
function test() external view onlyOwner anotherModifier { /* ... */ }
payable
functions are part of what makes Solidity and Ethereum so cool — they are a special type of function that can receive Ether.
Let that sink in for a minute. When you call an API function on a normal web server, you can’t send US dollars along with your function call — nor can you send Bitcoin.
But in Ethereum, because both the money (Ether), the data (transaction payload), and the contract code itself all live on Ethereum, it’s possible for you to call a function and pay money to the contract at the same time.
This allows for some really interesting logic, like requiring a certain payment to the contract in order to execute a function.
contract OnlineStore {
function buySomething() external payable {
// Check to make sure 0.001 ether was sent to the function call:
require(msg.value == 0.001 ether);
// If so, some logic to transfer the digital item to the caller of the function:
transferThing(msg.sender);
}
}
Here, msg.value
is a way to see how much Ether was sent to the contract, and ether
is a built-in unit.
What happens here is that someone would call the function from web3.js (from the DApp’s JavaScript front-end) as follows:
// Assuming `OnlineStore` points to your contract on Ethereum:
OnlineStore.buySomething({from: web3.eth.defaultAccount, value: web3.utils.toWei(0.001)})
Notice the value
field, where the javascript function call specifies how much ether
to send (0.001). If you think of the transaction like an envelope, and the parameters you send to the function call are the contents of the letter you put inside, then adding a value
is like putting cash inside the envelope — the letter and the money get delivered together to the recipient.
Note: If a function is not marked
payable
and you try to send Ether to it as above, the function will reject your transaction.
Withdraws
After you send Ether to a contract, it gets stored in the contract’s Ethereum account, and it will be trapped there — unless you add a function to withdraw the Ether from the contract.
You can write a function to withdraw Ether from the contract as follows:
contract GetPaid is Ownable {
function withdraw() external onlyOwner {
address payable _owner = address(uint160(owner()));
_owner.transfer(address(this).balance);
}
}
Note that we’re using owner()
and onlyOwner
from the Ownable
contract, assuming that was imported.
And most important for _owner
variable that it’s have to be a address payable
type for doing a sending and transferring ether instruction.
But our owner()
isn’t a type address payable
so we have to explicitly cast to address payable
. Casting any integer type like uint160
to address produces an address payable
.
You can transfer Ether to an address using the transfer
function, and address(this).balance
will return the total balance stored on the contract. So if 100 users had paid 1 Ether to our contract, address(this).balance
would equal 100 Ether.
You can use transfer
to send funds to any Ethereum address. For example, you could have a function that transfers Ether back to the msg.sender
if they overpaid for an item:
uint itemFee = 0.001 ether;
msg.sender.transfer(msg.value - itemFee);
Or in a contract with a buyer and a seller, you could save the seller’s address in storage, then when someone purchases his item, transfer him the fee paid by the buyer: seller.transfer(msg.value)
.
These are some examples of what makes Ethereum programming really cool — you can have decentralized marketplaces like this that aren’t controlled by anyone.
Random number generation via keccak256
The best source of randomness we have in Solidity is the keccak256
hash function.
We could do something like the following to generate a random number:
// Generate a random number between 1 and 100:
uint randNonce = 0;
uint random = uint(keccak256(abi.encodePacked(now, msg.sender, randNonce))) % 100;
randNonce++;
uint random2 = uint(keccak256(abi.encodePacked(now, msg.sender, randNonce))) % 100;
What this would do is take the timestamp of now
, the msg.sender
, and an incrementing nonce
(a number that is only ever used once, so we don’t run the same hash function with the same input parameters twice).
It would then “pack” the inputs and use keccak
to convert them to a random hash. Next, it would convert that hash to a uint
, and then use % 100
to take only the last 2 digits. This will give us a totally random number between 0 and 99.
Lesson 5
ERC721 Standard
Let’s take a look at the ERC721 standard:
contract ERC721 {
event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId);
event Approval(address indexed _owner, address indexed _approved, uint256 indexed _tokenId);
function balanceOf(address _owner) external view returns (uint256);
function ownerOf(uint256 _tokenId) external view returns (address);
function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
function approve(address _approved, uint256 _tokenId) external payable;
}
BalanceOf
function balanceOf(address _owner) external view returns (uint256 _balance);
This function simply takes an address
, and returns how many tokens that address
owns.
OwnerOf
function ownerOf(uint256 _tokenId) external view returns (address _owner);
This function takes a token ID (in our case, a Zombie ID), and returns the address
of the person who owns it.
Using SafeMath
OpenZeppelin has created a *library* called SafeMath that prevents these issues by default.
But before we get into that… What’s a library?
A *library* is a special type of contract in Solidity. One of the things it is useful for is to attach functions to native data types.
For example, with the SafeMath library, we’ll use the syntax using SafeMath for uint256
. The SafeMath library has 4 functions — add
, sub
, mul
, and div
. And now we can access these functions from uint256
as follows:
using SafeMath for uint256;
uint256 a = 5;
uint256 b = a.add(3); // 5 + 3 = 8
uint256 c = a.mul(2); // 5 * 2 = 10
Let’s take a look at the code behind SafeMath:
library SafeMath {
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
if (a == 0) {
return 0;
}
uint256 c = a * b;
assert(c / a == b);
return c;
}
function div(uint256 a, uint256 b) internal pure returns (uint256) {
// assert(b > 0); // Solidity automatically throws when dividing by 0
uint256 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return c;
}
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
assert(b <= a);
return a - b;
}
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
assert(c >= a);
return c;
}
}
First we have the library
keyword — libraries are similar to contract
s but with a few differences. For our purposes, libraries allow us to use the using
keyword, which automatically tacks on all of the library’s methods to another data type:
using SafeMath for uint;
// now we can use these methods on any uint
uint test = 2;
test = test.mul(3); // test now equals 6
test = test.add(5); // test now equals 11
Note that the mul
and add
functions each require 2 arguments, but when we declare using SafeMath for uint
, the uint
we call the function on (test
) is automatically passed in as the first argument.
Let’s look at the code behind add
to see what SafeMath does:
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
assert(c >= a);
return c;
}
Basically add
just adds 2 uint
s like +
, but it also contains an assert
statement to make sure the sum is greater than a
. This protects us from overflows.
assert
is similar to require
, where it will throw an error if false. The difference between assert
and require
is that require
will refund the user the rest of their gas when a function fails, whereas assert
will not. So most of the time you want to use require
in your code; assert
is typically used when something has gone horribly wrong with the code (like a uint
overflow).
So, simply put, SafeMath’s add
, sub
, mul
, and div
are functions that do the basic 4 math operations, but throw an error if an overflow or underflow occurs.
Syntax for comments
Commenting in Solidity is just like JavaScript. You’ve already seen some examples of single line comments throughout the CryptoZombies lessons:
// This is a single-line comment. It's kind of like a note to self (or to others)
Just add double //
anywhere and you’re commenting. It’s so easy that you should do it all the time.
But I hear you — sometimes a single line is not enough. You are born a writer, after all!
Thus we also have multi-line comments:
contract CryptoZombies {
/* This is a multi-lined comment. I'd like to thank all of you
who have taken your time to try this programming course.
I know it's free to all of you, and it will stay free
forever, but we still put our heart and soul into making
this as good as it can be.
Know that this is still the beginning of Blockchain development.
We've come very far but there are so many ways to make this
community better. If we made a mistake somewhere, you can
help us out and open a pull request here:
https://github.com/loomnetwork/cryptozombie-lessons
Or if you have some ideas, comments, or just want to say
hi - drop by our Telegram community at https://t.me/loomnetwork
*/
}
In particular, it’s good practice to comment your code to explain the expected behavior of every function in your contract. This way another developer (or you, after a 6 month hiatus from a project!) can quickly skim and understand at a high level what your code does without having to read the code itself.
The standard in the Solidity community is to use a format called *natspec*, which looks like this:
/// @title A contract for basic math operations
/// @author Dylan
/// @notice For now, this contract just adds a multiply function
contract Math {
/// @notice Multiplies 2 numbers together
/// @param x the first uint.
/// @param y the second uint.
/// @return z the product of (x * y)
/// @dev This function does not currently check for overflows
function multiply(uint x, uint y) returns (uint z) {
// This is just a normal comment, and won't get picked up by natspec
z = x * y;
}
}
@title
and @author
are straightforward.
@notice
explains to a user what the contract / function does. @dev
is for explaining extra details to developers.
@param
and @return
are for describing what each parameter and return value of a function are for.
Note that you don’t always have to use all of these tags for every function — all tags are optional. But at the very least, leave a @dev
note explaining what each function does.
Lesson 6
Intro to Web3.js
The Ethereum network is made up of nodes, with each containing a copy of the blockchain. When you want to call a function on a smart contract, you need to query one of these nodes and tell it:
- The address of the smart contract
- The function you want to call, and
- The variables you want to pass to that function.
Ethereum nodes only speak a language called *JSON-RPC*, which isn’t very human-readable.
Depending on your project’s workflow, you can add Web3.js to your project using most package tools:
// Using NPM
npm install web3
// Using Yarn
yarn add web3
// Using Bower
bower install web3
// ...etc.
Or you can simply download the minified .js
file from github and include it in your project:
<script language="javascript" type="text/javascript" src="web3.min.js"></script>
Web3 Providers
The first thing we need is a *Web3 Provider*.
Remember, Ethereum is made up of *nodes* that all share a copy of the same data. Setting a Web3 Provider in Web3.js tells our code which node we should be talking to handle our reads and writes. It’s kind of like setting the URL of the remote web server for your API calls in a traditional web app.
You could host your own Ethereum node as a provider. However, there’s a third-party service that makes your life easier so you don’t need to maintain your own Ethereum node in order to provide a DApp for your users.
(1) Infura
Infura is a service that maintains a set of Ethereum nodes with a caching layer for fast reads, which you can access for free through their API. Using Infura as a provider, you can reliably send and receive messages to/from the Ethereum blockchain without needing to set up and maintain your own node.
You can set up Web3 to use Infura as your web3 provider as follows:
var web3 = new Web3(new Web3.providers.WebsocketProvider("wss://mainnet.infura.io/ws"));
However, since our DApp is going to be used by many users — and these users are going to WRITE to the blockchain and not just read from it — we’ll need a way for these users to sign transactions with their private key.
Note: Ethereum (and blockchains in general) use a public / private key pair to digitally sign transactions. Think of it like an extremely secure password for a digital signature. That way if I change some data on the blockchain, I can prove via my public key that I was the one who signed it — but since no one knows my private key, no one can forge a transaction for me.
Cryptography is complicated, so unless you’re a security expert and you really know what you’re doing, it’s probably not a good idea to try to manage users’ private keys yourself in our app’s front-end.
But luckily you don’t need to — there are already services that handle this for you. The most popular of these is *Metamask*.
(2) Metamask
Metamask is a browser extension for Chrome and Firefox that lets users securely manage their Ethereum accounts and private keys, and use these accounts to interact with websites that are using Web3.js. (If you haven’t used it before, you’ll definitely want to go and install it — then your browser is Web3 enabled, and you can now interact with any website that communicates with the Ethereum blockchain!).
And as a developer, if you want users to interact with your DApp through a website in their web browser, you’ll definitely want to make it Metamask-compatible.
Note: Metamask uses Infura’s servers under the hood as a web3 provider, just like we did above — but it also gives the user the option to choose their own web3 provider. So by using Metamask’s web3 provider, you’re giving the user a choice, and it’s one less thing you have to worry about in your app.
Using Metamask’s web3 provider
Metamask injects their web3 provider into the browser in the global JavaScript object web3
. So your app can check to see if web3
exists, and if it does use web3.currentProvider
as its provider.
Here’s some template code provided by Metamask for how we can detect to see if the user has Metamask installed, and if not tell them they’ll need to install it to use our app:
window.addEventListener('load', function() {
// Checking if Web3 has been injected by the browser (Mist/MetaMask)
if (typeof web3 !== 'undefined') {
// Use Mist/MetaMask's provider
web3js = new Web3(web3.currentProvider);
} else {
// Handle the case where the user doesn't have web3. Probably
// show them a message telling them to install Metamask in
// order to use our app.
}
// Now you can start your app & access web3js freely:
startApp()
})
You can use this boilerplate code in all the apps you create in order to require users to have Metamask to use your DApp.
Note: There are other private key management programs your users might be using besides MetaMask, such as the web browser Mist. However, they all implement a common pattern of injecting the variable
web3
, so the method we describe here for detecting the user’s web3 provider will work for these as well.
Talking to Contracts
Now that we’ve initialized Web3.js with MetaMask’s Web3 provider, let’s set it up to talk to our smart contract.
Web3.js will need 2 things to talk to your contract: its address*** and its **ABI*.
(1) Contract Address
After you finish writing your smart contract, you will compile it and deploy it to Ethereum.
After you deploy your contract, it gets a fixed address on Ethereum where it will live forever. If you recall from Lesson 2, the address of the CryptoKitties contract on Ethereum mainnet is 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d
.
You’ll need to copy this address after deploying in order to talk to your smart contract.
(2) Contract ABI
The other thing Web3.js will need to talk to your contract is its *ABI*.
ABI stands for Application Binary Interface. Basically it’s a representation of your contracts’ methods in JSON format that tells Web3.js how to format function calls in a way your contract will understand.
When you compile your contract to deploy to Ethereum, the Solidity compiler will give you the ABI, so you’ll need to copy and save this in addition to the contract address.
Once you have your contract’s address and ABI, you can instantiate it in Web3 as follows:
// Instantiate myContract
var myContract = new web3js.eth.Contract(myABI, myContractAddress);
Calling Contract Functions
Our contract is all set up! Now we can use Web3.js to talk to it.
Web3.js has two methods we will use to call functions on our contract: call
and send
.
(1) Call
call
is used for view
and pure
functions. It only runs on the local node, and won’t create a transaction on the blockchain.
Review:
view
andpure
functions are read-only and don’t change state on the blockchain. They also don’t cost any gas, and the user won’t be prompted to sign a transaction with MetaMask.
Using Web3.js, you would call
a function named myMethod
with the parameter 123
as follows:
myContract.methods.myMethod(123).call()
(2) Send
send
will create a transaction and change data on the blockchain. You’ll need to use send
for any functions that aren’t view
or pure
.
Note:
send
ing a transaction will require the user to pay gas, and will pop up their Metamask to prompt them to sign a transaction. When we use Metamask as our web3 provider, this all happens automatically when we callsend()
, and we don’t need to do anything special in our code. Pretty cool!
Using Web3.js, you would send
a transaction calling a function named myMethod
with the parameter 123
as follows:
myContract.methods.myMethod(123).send()
Metamask & Accounts
MetaMask allows the user to manage multiple accounts in their extension.
We can see which account is currently active on the injected web3
variable via:
var userAccount = web3.eth.accounts[0]
Because the user can switch the active account at any time in MetaMask, our app needs to monitor this variable to see if it has changed and update the UI accordingly.
We can do that with a setInterval
loop as follows:
var accountInterval = setInterval(function() {
// Check if account has changed
if (web3.eth.accounts[0] !== userAccount) {
userAccount = web3.eth.accounts[0];
// Call some function to update the UI with the new account
updateInterface();
}
}, 100);
What this does is check every 100 milliseconds to see if userAccount
is still equal web3.eth.accounts[0]
(i.e. does the user still have that account active). If not, it reassigns userAccount
to the currently active account, and calls a function to update the display.