Solidity Notes
Solidity Notes
Mapping
In Lesson 1 we looked at structs and arrays. Mappings are another way of
storing organized data in Solidity.
// 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;
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.
Require statement
require makes it so that the function will throw an error and stop executing if
some condition is not true:
Thus require is quite useful for verifying certain conditions that must be true
before running a function.
Inheritance:
One feature of Solidity that makes this more manageable is
contract inheritance:
contract Doge {
function catchphrase() public returns (string memory) {
return "So Wow CryptoDoge";
}
}
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.
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;
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.
For now, it's enough to understand that there are cases where you'll need to
explicitly declare storage or memory!
internal is the same as private, except that it's also accessible to contracts
that inherit from this contract. (Hey, that sounds like what we want here!).
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.
Ownable contracts
setKittyContractAddress is external, so anyone can call it! That means
anyone who called the function could change the address of the CryptoKitties
contract, and break our app for all its users.
We do want the ability to update this address in our contract, but we don't
want everyone to be able to update it.
To handle cases like this, one common practice that has emerged is to make
contracts Ownable — meaning they have an owner (you) who has special
privileges.
Constructors:
constructor() 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.
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
The creators of Ethereum wanted to make sure someone couldn't clog up the
network with an infinite loop, or hog all the network resources with really
intensive computations. So they made it so transactions aren't free, and users
have to pay for computation time as well as storage.
Struct packing to save gas
In Lesson 1, we mentioned that there are other types
of uints: uint8, uint16, uint32, etc.
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!
This way we can pass a reference to our zombie into a function instead of
passing in a zombie ID and looking it up.
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.
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.
FUNCTION MODIFIERES
We have visibility modifiers that control when and where the function can
be called from: private means it's only callable from other functions inside
the contract; internal is like private but can also be called by contracts
that inherit from this one; external can only be called outside the contract;
and finally public can be called anywhere, both internally and externally.
1. We also have state modifiers, which tell us how the function interacts
with the BlockChain: view tells us that by running the function, no data
will be saved/changed. pure tells us that not only does the function not
save any data to the blockchain, but it also doesn't read any data from
the blockchain. Both of these don't cost any gas to call if they're called
externally from outside the contract (but they do cost gas if called
internally by another function).
The payable Modifier
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.
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:
It is important to note that you cannot transfer Ether to an address unless that
address is of type address payable. But the _owner variable is of type uint160,
meaning that we must explicitly cast it to address payable.
Once you cast the address from uint160 to address payable, you can transfer
Ether to that 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.
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.
Once a node has solved the PoW, the other nodes stop trying to solve the
PoW, verify that the other node's list of transactions are valid, and then accept
the block and move on to trying to solve the next block.
Let's say we had a coin flip contract — heads you double your money, tails
you lose everything. Let's say it used the above random function to determine
heads or tails. (random >= 50 is heads, random < 50 is tails).
Because we're just building a simple game for demo purposes in this tutorial
and there's no real money on the line, we're going to accept the tradeoffs of
using a random number generator that is simple to implement, knowing that it
isn't totally secure.
Tokens
A token on Ethereum is basically just a smart contract that follows some
common rules — namely it implements a standard set of functions that all
other token contracts share, such as transferFrom(address _from, address
_to, uint256 _tokenId) and balanceOf(address _owner).
So basically a token is just a contract that keeps track of who owns how much
of that token, and some functions so those users can transfer their tokens to
other addresses.
Since all ERC20 tokens share the same set of functions with the same names,
they can all be interacted with in the same ways.
This means if you build an application that is capable of interacting with one
ERC20 token, it's also capable of interacting with any ERC20 token. That way
more tokens can easily be added to your app in the future without needing to
be custom coded. You could simply plug in the new token contract address,
and boom, your app has another token it can use.
The exchange only needs to implement this transfer logic once, then when it
wants to add a new ERC20 token, it's simply a matter of adding the new
contract address to its database.
contract ERC721 {
event Transfer(address indexed _from, address indexed _to, uint256 indexed
_tokenId);
event Approval(address indexed _owner, address indexed _approved, uint256
indexed _tokenId);
This is the list of methods we'll need to implement, which we'll be doing over
the coming chapters in pieces.
It looks like a lot, but don't get overwhelmed! We're here to walk you through
it.
Implementing a token contract
When implementing a token contract, the first thing we do is copy the interface
to its own Solidity file and import it, import "./erc721.sol";. Then we have our
contract inherit from it, and we override each method with a function definition.
Luckily in Solidity, your contract can inherit from multiple contracts as follows:
contract SatoshiNakamoto is NickSzabo, HalFinney {
// Omg, the secrets of the universe revealed!
}
As you can see, when using multiple inheritance, you just separate the
multiple contracts you're inheriting from with a comma, ,. In this case, our
contract is inheriting from NickSzabo and HalFinney.
We've gone ahead and copied the empty shell of all the functions you'll be
implementing in this lesson.
balanceOf
function balanceOf(address _owner) external view returns (uint256 _balance);
This function simply takes an address, and returns how many tokens
that address owns.
In our case, our "tokens" are Zombies. Do you remember where in our DApp
we stored how many zombies an owner has?
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.
Again, this is very straightforward for us to implement, since we already have
a mapping in our DApp that stores this information. We can implement this
function in one line, just a return statement.
Note: Remember, uint256 is equivalent to uint. We've been using uint in our
code up until now, but we're using uint256 here because we copy/pasted from
the spec.
What's an overflow?
Let's say we have a uint8, which can only have 8 bits. That means the largest
number we can store is binary 11111111 (or in decimal, 2^8 - 1 = 255).
Using SafeMath
To prevent this, OpenZeppelin has created a library called SafeMath that
prevents these issues by default.
uint256 a = 5;
uint256 b = a.add(3); // 5 + 3 = 8
uint256 c = a.mul(2); // 5 * 2 = 10
We'll look at what these functions do in the next chapter, but for now let's add
the SafeMath library to our contract.