Home

Lesson 2

Addresses

  • ETH blockchain is made up of accounts which you can think of as a bank account.
  • Balance is Ether.
  • Each account has an address.

For now, 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 of our zombies. When a user creates new zombies by interacting with our app, we'll set ownership of those zombies to the Ethereum address that called the function.

Mappings

We've 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;

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.

pragma solidity >=0.5.0 <0.6.0; contract ZombieFactory { event NewZombie(uint zombieId, string name, uint dna); uint dnaDigits = 16; uint dnaModulus = 10 ** dnaDigits; struct Zombie { string name; uint dna; } Zombie[] public zombies; mapping (uint => address) public zombieToOwner; mapping (address => uint) ownerZombieCount; function _createZombie(string memory _name, uint _dna) private { uint id = zombies.push(Zombie(_name, _dna)) - 1; emit NewZombie(id, _name, _dna); } function _generateRandomDna(string memory _str) private view returns (uint) { uint rand = uint(keccak256(abi.encodePacked(_str))); return rand % dnaModulus; } function createRandomZombie(string memory _name) public { uint randDna = _generateRandomDna(_name); _createZombie(_name, randDna); } }

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.

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 the code example of assigning a zombie to the owner and increasing their count:

pragma solidity >=0.5.0 <0.6.0; contract ZombieFactory { event NewZombie(uint zombieId, string name, uint dna); uint dnaDigits = 16; uint dnaModulus = 10 ** dnaDigits; struct Zombie { string name; uint dna; } Zombie[] public zombies; mapping (uint => address) public zombieToOwner; mapping (address => uint) ownerZombieCount; function _createZombie(string memory _name, uint _dna) private { uint id = zombies.push(Zombie(_name, _dna)) - 1; zombieToOwner[id] = msg.sender; ownerZombieCount[msg.sender]++; emit NewZombie(id, _name, _dna); } function _generateRandomDna(string memory _str) private view returns (uint) { uint rand = uint(keccak256(abi.encodePacked(_str))); return rand % dnaModulus; } function createRandomZombie(string memory _name) public { uint randDna = _generateRandomDna(_name); _createZombie(_name, randDna); } }

require

How can we make it so this 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!"; }

In an example that only allows a user to create one zombie:

pragma solidity >=0.5.0 <0.6.0; contract ZombieFactory { event NewZombie(uint zombieId, string name, uint dna); uint dnaDigits = 16; uint dnaModulus = 10 ** dnaDigits; struct Zombie { string name; uint dna; } Zombie[] public zombies; mapping (uint => address) public zombieToOwner; mapping (address => uint) ownerZombieCount; function _createZombie(string memory _name, uint _dna) private { uint id = zombies.push(Zombie(_name, _dna)) - 1; zombieToOwner[id] = msg.sender; ownerZombieCount[msg.sender]++; emit NewZombie(id, _name, _dna); } function _generateRandomDna(string memory _str) private view returns (uint) { uint rand = uint(keccak256(abi.encodePacked(_str))); return rand % dnaModulus; } function createRandomZombie(string memory _name) public { require(ownerZombieCount[msg.sender] == 0); uint randDna = _generateRandomDna(_name); _createZombie(_name, randDna); } }

Inheritance

Our game code is getting quite long. Rather than making one extremely long contract, sometimes it makes sense to split your code logic across multiple contracts to organize the code.

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"; } }

An example with the doge:

pragma solidity >=0.5.0 <0.6.0; contract ZombieFactory { event NewZombie(uint zombieId, string name, uint dna); uint dnaDigits = 16; uint dnaModulus = 10 ** dnaDigits; struct Zombie { string name; uint dna; } Zombie[] public zombies; mapping (uint => address) public zombieToOwner; mapping (address => uint) ownerZombieCount; function _createZombie(string memory _name, uint _dna) private { uint id = zombies.push(Zombie(_name, _dna)) - 1; zombieToOwner[id] = msg.sender; ownerZombieCount[msg.sender]++; emit NewZombie(id, _name, _dna); } function _generateRandomDna(string memory _str) private view returns (uint) { uint rand = uint(keccak256(abi.encodePacked(_str))); return rand % dnaModulus; } function createRandomZombie(string memory _name) public { require(ownerZombieCount[msg.sender] == 0); uint randDna = _generateRandomDna(_name); _createZombie(_name, randDna); } } contract ZombieFeeding is ZombieFactory { }

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.

import "./someothercontract.sol"; contract newContract is SomeOtherContract { }

Storage vs Memory

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. } }

More on Function Visibility with Internal/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. (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.

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(); } }

The code to update our _createZombie from private to internal:

pragma solidity >=0.5.0 <0.6.0; contract ZombieFactory { event NewZombie(uint zombieId, string name, uint dna); uint dnaDigits = 16; uint dnaModulus = 10 ** dnaDigits; struct Zombie { string name; uint dna; } Zombie[] public zombies; mapping (uint => address) public zombieToOwner; mapping (address => uint) ownerZombieCount; // edit function definition below function _createZombie(string memory _name, uint _dna) internal { uint id = zombies.push(Zombie(_name, _dna)) - 1; zombieToOwner[id] = msg.sender; ownerZombieCount[msg.sender]++; emit NewZombie(id, _name, _dna); } function _generateRandomDna(string memory _str) private view returns (uint) { uint rand = uint(keccak256(abi.encodePacked(_str))); return rand % dnaModulus; } function createRandomZombie(string memory _name) public { require(ownerZombieCount[msg.sender] == 0); uint randDna = _generateRandomDna(_name); _createZombie(_name, randDna); } }

Interacting with other contracts

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); }

  1. 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.
  2. 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.

Based on the following contract:

function getKitty(uint256 _id) external view returns ( bool isGestating, bool isReady, uint256 cooldownIndex, uint256 nextActionAt, uint256 siringWithId, uint256 birthTime, uint256 matronId, uint256 sireId, uint256 generation, uint256 genes ) { Kitty storage kit = kitties[_id]; // if this variable is 0 then it's not gestating isGestating = (kit.siringWithId != 0); isReady = (kit.cooldownEndBlock <= block.number); cooldownIndex = uint256(kit.cooldownIndex); nextActionAt = uint256(kit.cooldownEndBlock); siringWithId = uint256(kit.siringWithId); birthTime = uint256(kit.birthTime); matronId = uint256(kit.matronId); sireId = uint256(kit.sireId); generation = uint256(kit.generation); genes = kit.genes; }

... we define an interface in our code to talk to that contract ...

pragma solidity >=0.5.0 <0.6.0; import "./zombiefactory.sol"; contract KittyInterface { function getKitty(uint256 _id) external view returns ( bool isGestating, bool isReady, uint256 cooldownIndex, uint256 nextActionAt, uint256 siringWithId, uint256 birthTime, uint256 matronId, uint256 sireId, uint256 generation, uint256 genes ); } contract ZombieFeeding is ZombieFactory { function feedAndMultiply(uint _zombieId, uint _targetDna) public { require(msg.sender == zombieToOwner[_zombieId]); Zombie storage myZombie = zombies[_zombieId]; _targetDna = _targetDna % dnaModulus; uint newDna = (myZombie.dna + _targetDna) / 2; _createZombie("NoName", newDna); } }

Using an interface

Continuing our previous example with NumberInterface, once we've defined the interface as:

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 } }

With our code, we are provided the address for the Kitty contract, which we can use:

pragma solidity >=0.5.0 <0.6.0; import "./zombiefactory.sol"; contract KittyInterface { function getKitty(uint256 _id) external view returns ( bool isGestating, bool isReady, uint256 cooldownIndex, uint256 nextActionAt, uint256 siringWithId, uint256 birthTime, uint256 matronId, uint256 sireId, uint256 generation, uint256 genes ); } contract ZombieFeeding is ZombieFactory { address ckAddress = 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d; KittyInterface kittyContract = KittyInterface(ckAddress); function feedAndMultiply(uint _zombieId, uint _targetDna) public { require(msg.sender == zombieToOwner[_zombieId]); Zombie storage myZombie = zombies[_zombieId]; _targetDna = _targetDna % dnaModulus; uint newDna = (myZombie.dna + _targetDna) / 2; _createZombie("NoName", newDna); } }

Handling multiple return values

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(); }

In our example, we want to get back genes from the Kitty contract. The code looks like so:

pragma solidity >=0.5.0 <0.6.0; import "./zombiefactory.sol"; contract KittyInterface { function getKitty(uint256 _id) external view returns ( bool isGestating, bool isReady, uint256 cooldownIndex, uint256 nextActionAt, uint256 siringWithId, uint256 birthTime, uint256 matronId, uint256 sireId, uint256 generation, uint256 genes ); } contract ZombieFeeding is ZombieFactory { address ckAddress = 0x06012c8cf97BEaD5deAe237070F9587f8E7A266d; KittyInterface kittyContract = KittyInterface(ckAddress); function feedAndMultiply(uint _zombieId, uint _targetDna) public { require(msg.sender == zombieToOwner[_zombieId]); Zombie storage myZombie = zombies[_zombieId]; _targetDna = _targetDna % dnaModulus; uint newDna = (myZombie.dna + _targetDna) / 2; _createZombie("NoName", newDna); } function feedOnKitty(uint _zombieId, uint _kittyId) public { uint kittyDna; (,,,,,,,,,kittyDna) = kittyContract.getKitty(_kittyId); feedAndMultiply(_zombieId, kittyDna); } }

Note: It's a little bit messy with the (,,,,,,,,,kittyDna) assignment.

Example JS Implementation

var abi = /* abi generated by the compiler */ var ZombieFeedingContract = web3.eth.contract(abi) var contractAddress = /* our contract address on Ethereum after deploying */ var ZombieFeeding = ZombieFeedingContract.at(contractAddress) // Assuming we have our zombie's ID and the kitty ID we want to attack let zombieId = 1; let kittyId = 1; // To get the CryptoKitty's image, we need to query their web API. This // information isn't stored on the blockchain, just their webserver. // If everything was stored on a blockchain, we wouldn't have to worry // about the server going down, them changing their API, or the company // blocking us from loading their assets if they don't like our zombie game ;) let apiUrl = "https://api.cryptokitties.co/kitties/" + kittyId $.get(apiUrl, function(data) { let imgUrl = data.image_url // do something to display the image }) // When the user clicks on a kitty: $(".kittyImage").click(function(e) { // Call our contract's `feedOnKitty` method ZombieFeeding.feedOnKitty(zombieId, kittyId) }) // Listen for a NewZombie event from our contract so we can display it: ZombieFactory.NewZombie(function(error, result) { if (error) return // This function will display the zombie, like in lesson 1: generateZombie(result.zombieId, result.name, result.dna) })