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.
Our contract is almost finished! Now let's add an event.
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
})
Solidity contract is complete! Now we need to write a JavaScript frontend that interacts with the contract.
Ethereum has a JavaScript library called Web3.js.
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
Understand that an address is owned by a specific user (or a smart contract).
So we can use it as a unique ID for ownership . When a user creates new entity/object by interacting with an app, will set ownership of those entity/object to the Ethereum address that called the function.
Mappings
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
How can we make function can only be called once per player?
For that we use 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.
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
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.
}
}