--- tags: Labs --- # Lab 2: Introduction to Solidity :::info **Released:** Friday March 25, 2022. **Due:** Sunday April 24, 2022 at 11:59PM ET. ::: ## Introduction Welcome to the Solidity lab! This assignment will help you set up your development environment and get you up to speed with the language you'll use to develop smart contracts for blockchains using the Ethereum Virtual Machine (EVM). You'll also use Solidity for the last course project: Swap! Solidity may look similar to languages you've used before, but there are several *quirks and features* that make it a bit tricky. If you have any questions about this language, please ask the TA staff on EdStem or through the `#solidity-lab` Discord channel! :::warning **We know this handout is quite long**, but we wanted to provide a one-stop guide to Solidity for all the smart contract development you'll do in CS1951L. Even if you're already proficient in the language, everybody benefits from a brief refresher; we recommend you at least skim through our guide before jumping into the assignment. ::: [ToC] ## Setup and Handin ### Setup Before we can start writing smart contracts, we must first set up your development environment. After following the setup instructions below, **[clone the stencil](https://classroom.github.com/a/TQqeLFcY) from Github Classroom to get started on the assignment.** #### Hardhat We will use a framework called [Hardhat](https://hardhat.org/) to help us build smart contracts for blockchains using the EVM. Hardhat will help us compile, deploy, test, and debug our smart contracts. :::warning **You must have Node installed before you can use Hardhat.** You can check by running `node -v` in the terminal. If you see `v16.14.2` or greater, you're good to go! If `node -v` returns an error or a version lower than `v16.14.2`, you must install Node from [here](https://nodejs.org/en/). Download the "v16.14.2 LTS" version. ::: ##### Hardhat project structure: Hardhat projects typically use the following file structure: - `contracts/`: Directory for your Solidity contracts - `test/`: Directory for test files for testing your contracts - `hardhat.config.js`: Hardhat configuration file ##### Basic Hardhat development workflow: We will now go over a basic, high-level workflow for developing a new smart contract with Hardhat. **For this assignment, we've already set up the project for you; don't worry about these steps.** ###### Setup the Hardhat project: - Initialize a new npm project by going to an empty folder and running `npm init -y`. - Once your project is ready, install Hardhat by running `npm install --save-dev hardhat`. - Create a blank Hardhat project: `npx hardhat` - Select `Create a basic sample project`. You can choose the default options for all the prompts, especially the one asking to install npm packages. - This scaffolds a basic project; feel free to use the sample contract and test files as starting points. ###### Create a contract: - Move into the `contracts/` directory: `cd contracts` - Create a blank Solidity file: `touch YourContractName.sol` - A `.sol` file has just been created in the `contracts/` directory. Open this in your favorite text editor and write the contract code. ###### Create a test: - Move into the `test/` directory: `../test` - Create a test file: `touch your-test-name.js` - A `.js` file has just been created in the `test/` directory. You can learn more about writing tests with Hardhat [here](https://hardhat.org/tutorial/testing-contracts.html). - Run your tests: `npx hardhat test`. There's much more to Hardhat than we covered here, so we highly recommend you look through the [Hardhat Documentation](https://hardhat.org/getting-started/) to get a better idea of everything you can do. We promise it's worth a look! #### Remix [Remix](https://remix-project.org/) is an IDE that allows developing, debugging, testing, deploying, and administering smart contracts for EVM blockchains. It looks similar to VSCode (it uses Monoco, the same text editor as VSCode), but it runs in the browser and has tons of features geared towards smart contract development. To work with local projects, we must first connect your file system to the Remix IDE. To do so, we will use the `remixd` NPM package. Install it by running `npm install -g @remix-project/remixd` in your terminal (again, you may need to use `sudo` if you're on Mac). After you install `remixd`, you can now use it to create a bridge between your project and the Remix IDE. In your terminal, run the following command: `remixd -s <absolute-path-to-the-shared-folder> --remix-ide https://remix.ethereum.org`, replacing the shared folder path with the *absolute* path to your Hardhat project (the repo you cloned). Now, [open the Remix IDE](https://remix.ethereum.org/) in your browser, expand the "Workspaces" dropdown under the File Explore sidebar, click "- connect to localhost -", and click "Connect". This will open your project directory in the Remix window. Yay! You can learn all about Remix's various features [here](https://remix-ide.readthedocs.io/en/latest/). They're super helpful for smart contract development within CS1951L and beyond! #### VSCode More of a VSCode kind of person? We got you. If you would like to use VSCode to write Solidity, we recommend this [Solidity extension](https://marketplace.visualstudio.com/items?itemName=JuanBlanco.solidity), which provides syntax highlighting, autocomplete, and more! ### Handin - Ensure you have written code for the task in `contracts/Election.sol`. Make sure your error handling code reverts with the exact error messages used within the test suite or the autograder may deduct points. - We highly recommend using the Github submission method when submitting on Gradescope. You can also upload your submission as a zip file, but **please ensure you are zipping the contents of the project directory instead of the folder itself:** ![](https://i.imgur.com/hbGj8bv.png) - You may submit as many times as you want before the deadline. Only your latest submission will be graded (though you will spend late days if the latest submission is after the due date.) - Do not modify function headers or parts of the stencil. This ensures your submission works with the autograder. - Questions or concerns? Ask on Discord or Ed. ## Part 1: Solidity Crash Course This section will give a (not so) brief tour of the essential Solidity language features you'll need to know. Please use this section as a reference throughout the course; we think it will be very helpful! :::warning **Solidity is a new, continuously evolving language** that may result in syntax changes over time. Whenever new features or improvements are introduced, there is a new version. For this assignment, we will use **version 0.8.13** (the latest version at the time of writing). ::: ### Contracts Contracts are the fundamental building block of Ethereum applications -- they encapsulate all the variables and functions of a smart contract. They're analogous to classes in object-oriented languages. Unlike classes, contracts can be created and deployed on the blockchain. Once a contract is deployed, any function marked `public` will be available for any Ethereum user to interact with, and the blockchain will persist its state across transactions for the lifetime of the contract (which is forever 😲 until the contract is deleted). Blockchains are so cool! ```solidity= pragma solidity ^0.8.13; contract HelloWorld { function sayHello() public pure returns (string memory) { return "Hello, world!"; } } ``` Here we have a simple smart contract. At the top, we see `pragma solidity ^0.8.3;`. The `pragma` keyword specifies which compiler version to use and is typically the first line for any Solidity file. At the time of writing, the latest version of Solidity is `v0.8.3`, so we tell the compiler that the file won't compile with a version earlier than version 0.8.3, and it will also not work on a compiler starting from version 0.9.0 (that's what the `^` is doing). Next, we use the `contract` keyword to declare a new contract called `HelloWorld`. This contract contains one function named `sayHello`, which returns the string `"Hello, world!"` when called. Nice! You may ask, "what does `public` and `pure` and `memory` mean?" Don't worry, we'll get to that shortly! ### Variables Solidity is statically typed, which means that you must specify the variable's type during declaration. **There is no concept of "undefined" or "null" in Solidity,** which means you must also initialize all declared variables with a value based on its type. If you don't provide an initial value, variables will be assigned to the [default value](https://docs.soliditylang.org/en/v0.8.13/control-structures.html#scoping-and-declarations) for their type. #### State Variables State Variables are variables whose values are permanently stored in a contract's storage -- a data area that persists between function calls and transactions. Data stored here is stored on the blockchain, kind of like writing to a database. These can be initialized within a `constructor`, which is a function that's called once when the contract is first created on the blockchain. You can also assign an initial value during declaration. These values persist data through the contract's lifetime and are modified by the contract's functions. ```solidity= contract StateVariables { // State variables uint someData; uint luckyNumber = 42; // Initializing state variable in constructor constructor(uint _num) public { someData = _num; } } ``` State variables can have three types of scopes: `public`, `internal`, or `private`. The default visibility of state variables is `internal` . - `public` state variables can be accessed by internal contract code as well as **externally via messages**. Automatic getter functions are generated for public state variables. - `internal` state variables can be accessed internally from the **current contract or child contracts** (contracts that inherit the current contract), but not from external contracts or callers. - `private` state variables can be accessed internally from the contract they are defined in, **but not from child or external contracts**. ```solidity= contract ContractA { uint public data = 30; uint internal internalData= 10; // Functions are variables; the same scoping rules apply. function x() public returns (uint) { data = 3; // internal access return data; } } contract ContractB is ContractA { function y() public returns (uint) { internalData = 3; // internal access return internalData; } } contract Caller { ContractA a = new ContractA(); function f() public view returns (uint) { return a.data(); // external access } } ``` #### Local Variables Local Variables are variables that are only accessible within the scope of the function where they're defined. Parameters to a function are always local to that function. ```solidity= contract LocalVariables { function product() public returns (uint) { uint a = 3; uint b = 4; return a * b; // 12 } // a and b aren't defined outside product() } ``` #### Global Variables There are special variables defined in the global namespace that provide information about the blockchain and properties about the transaction calling your contract. Read more about all the special variables [here](https://docs.soliditylang.org/en/v0.8.11/units-and-global-variables.html#special-variables-and-functions). ### Gas Whenever someone calls your contract, they must pay a certain amount of gas: a fee required to successfully send a transaction or execute a contract on the Ethereum blockchain. The purpose of gas is to limit the amount of work needed to execute the transaction all while paying for the execution at the same time. While the transaction is executed in the EVM, the gas is depleted according to specifc rules (for example, certain operations cost more gas to execute than others). The gas price is a value set by the creator of the transaction, who has to pay `gas_price * gas` up front from the sending account. If some gas is left after the execution, it is refunded to the caller in the same way. **Transactions with a higher gas price get higher priority to be included in a block.** If the gas is used up at any point (i.e. it would be negative) before the transaction completes, an out-of-gas exception is triggered, which reverts all modifications made to the state in the current call. The spent gas is not refunded to the caller. ```solidity= contract Gas { uint public i = 0; function loopForever() public { // This loop goes on until we spend all of the gas // and the transaction fails. // When the transaction fails, all state changes are // reverted and gas spent is not refunded to the caller. while (true) { i += 1; } } } ``` #### Gas Limit There are two upper bounds to the maximum amount of gas you can spend: - `gas limit`: the maximum amount of gas you're willing to spend on a transaction. - `block gas limit`: the maximum amount of gas allowed in a block. This is determined by the network. ### Math Operations Math in Solidity is familiar. The following operations are the same as in many other programming languages: - Addition: `x + y` - Subtraction: `x - y` - Multiplication: `x * y` - Division: `x / y` - Modulus / remainder: `x % y` Solidity also supports the `**` exponential operator: ```solidity= uint x = 3 ** 4; // equal to 3^4 = 81 ``` ### Structs Sometimes you need a more complex data type. For this, Solidity provides structs: ```solidity= struct Champion { string name; uint health; uint mana; } ``` Structs allow you to combine multiple pieces of data into a single data type. You can access an individual member with the dot (`.`) syntax: ```solidity= Champion champion = Champion("Ashe", 100, 100); string name = champion.name; // "Ashe" ``` ### Arrays When you want a collection of data, use an array. There are two types of arrays in Solidity: fixed arrays and dynamic arrays. Fixed arrays can be declared as such: ```solidity= // Array with a fixed length of 2 elements: uint8[2] memory arr1 = [1, 2]; // initialized with default (zero) elements: uint[5] memory arr3; // [0, 0, 0, 0, 0] ``` Elements at a specific index can be accessed/modified as such: ```solidity= uint[3] memory arr = [1, 2, 3]; arr[0] = 10; // arr is now [10, 2, 3] uint num = arr[0]; // 10 ``` #### Dynamic Storage Arrays Storage arrays with dynamic lengths can be declared as state variables. These have no fixed size and can be resized by using `push()` and `pop()`: ```solidity= contract DynamicArray { uint[] arr; function x() public returns (uint) { // arr.length is 0 initially... arr.push(1); // arr is [1] arr.push(2); // arr is [1, 2] arr.push(3); // arr is [1, 2, 3] return arr.length; // arr.length is 3 } } ``` You can also create an array of structs. Using the Champion struct from before: ```solidity= Champion[] champions; // Dynamic array, we can keep adding to it! ``` Remember how state variables are permanently stored on the blockchain? Creating a dynamic storage array of structs, like this example, can be useful for storing structured data in your contracts (kind of like a database). #### Dynamic Memory Arrays Memory arrays with dynamic length can be created using the `new` operator. Unlike fixed arrays, you don't need to know the array's size at compile time; the size can be specified at runtime. However, unlike dynamic storage arrays, dynamic memory arrays cannot be resized once created (e.g. the `.push` member functions are not available and you cannot assign `.length`). You must either calculate the required size in advance or copy every element into a new memory array: ```solidity= function f(uint len) public pure { uint[] memory a = new uint[](7); bytes memory b = new bytes(len); assert(a.length == 7); assert(b.length == len); a[6] = 8; } ``` ### Data Locations In Solidity, you can store variables in two locations — in `storage` and in `memory`. **Storage** refers to data that's stored permanently on the blockchain. **Memory**, on the other hand, refers to temporary variables that are erased between external function calls to your contract. It's similar to the difference between your computer's hard disk vs. RAM. You don't need to use these keywords most of the time because Solidity handles them by default. State variables are always `storage` and written permanently to the blockchain. Local variables are by default `memory` and will disappear when the function call returns. However, there are times when you do need to use these keywords to explicitly define the data location, namely when using structs and arrays within functions: ```solidity= contract CookieFactory { struct Cookie { string name; string status; } Cookie[] cookies; function eatCookie(uint _index) public { // Cookie myCookie = cookies[_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: Cookie storage myCookie = cookies[_index]; // ...in which case `myCookie` is a pointer to `cookies[_index]` // in storage, and... myCookie.status = "Eaten!"; // ...this will permanently change `cookies[_index]` on the blockchain. // If you just want a copy, you can use `memory`: Cookie memory anotherCookie = cookies[_index + 1]; // ...in which case `anotherCookie` will simply be a copy of the // data in memory, and... anotherCookie.status = "Eaten!"; // ...will just modify the temporary variable and have no effect // on `cookies[_index + 1]`. But you can do this: cookies[_index + 1] = anotherCookie; // ...if you want to copy the changes back into blockchain storage. } } ``` No worries if you don't fully understand when to use `memory` versus` storage` -- throughout this lab, we'll tell you when to what, and the Solidity compiler will also give you warnings to let you know when you should be using one of these keywords. Right now, it's enough to understand that there are cases (mainly arrays and structs within functions) where you'll need to declare `storage` or `memory` explicitly! :::warning **Good Solidity code consumes as little Gas as possible.** Storing data in Memory consumes far less Gas compared to storing data in Storage (contract storage is very expensive to use). In the vast majority of cases, it's better to use Memory for intermediate calculations and store the final result in Storage. In other languages, you might want to store the result of an expensive calculation for further use down the line. However, in Solidity, it's possible that rerunning an expensive computation might still be much less costly than using Storage. ::: ### Functions As you've already seen in the previous examples, functions are defined with the `function` keyword as such: ```solidity= function eatCookie(string memory _name, uint _amount) public { // write code here... } ``` Here we have a function named `eatCookie` that takes two 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`: this means that this function is callable by any Ethereum user. In addition to the [visibility levels](#State-Variables) we discussed earlier, functions can also be marked `external`. Functions marked `external` can ONLY be called outside the contract — they can't be called by other functions inside that contract. In general, **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. Same as function parameters, it's convention to prefix the names of private functions with an underscore (`_`). We also instruct the compiler that the `_name` variable should be stored in `memory`. We must **explicitly state the data location for all reference types** such as `arrays`, `structs`, `mappings`, and `strings`. :::spoiler **What's a reference type?** To understand reference types, we must understand the two ways in which you can pass an argument to a Solidity function: - **By value**, which means that the Solidity compiler will pass a copy of the parameter's value to your function. This means your function can modify the value without worrying that the value of the initial parameter gets changed. - **By reference**, which means that your function is passed in reference to the original variable. Hence, if your function changes the value of the received variable, the value of the original variable also gets changed. Because complex types do not always fit into 256 bits, we have to handle them more carefully than value-types (uint, bool, etc.). Since copying them can be quite expensive, we have to think about whether we want them to be stored in `memory` (which is not persisting) or `storage` (where the state variables are held). ::: > :::info **Note:** By convention, function parameter variable names are prefixed with an underscore (`_`) to differentiate them from state and global variables. This is not required, but it's the convention we'll use throughout the course. ::: Functions are called as such: ```solidity= eatCookie("nathan", 100); ``` Just like Go, functions in Solidity can return multiple values: ```solidity= contract Test { function getResult() public pure returns(uint product, uint sum){ uint a = 1; // local variable uint b = 2; product_ = a * b; sum_ = a + b; //alternative return statement to return //multiple values //return(a*b, a+b); } } ``` #### View and Pure Functions **View functions** are declared with the `view` keyword and **cannot modify the state of the contract**; the compiler will throw an error if you try. Think of view functions as "read-only." More specifically, view functions cannot: - Modify state variables. - Emit events. - Create other contracts. - Call the built-in `selfdestruct` function. - Send Ether via calls. - Call any function which is not marked `view` or `pure`. - Use low-level calls. - Use inline assembly containing certain opcodes. The getter functions automatically provided by public state variables are by default view functions. ```solidity= contract Test { // Note the 'view' keyword function getResult() public view returns(uint product, uint sum){ uint a = 1; uint b = 2; product = a * b; sum = a + b; } } ``` **Pure functions** are even more restrictive than view functions; **they cannot read or modify the state of the contract.** Think of pure functions as static functions in other object-oriented languages. In addition to the restrictions on view functions, pure functions cannot: - Read state variables. - Access `address(this).balance` or `<address>.balance`. - Access any of the special variables `block`, `tx`, `msg` (although `msg.sig` and `msg.data` can be read). - Call other functions not marked pure. - Use inline assembly that contains certain opcodes. ```solidity= contract Test { // Same example, but note the 'pure' keyword function getResult() public pure returns(uint product, uint sum){ uint a = 1; uint b = 2; product = a * b; sum = a + b; } } ``` :::info **Note:** It may be hard to remember when to mark functions as `pure` or `view`. A general rule of thumb is to start at the most restrictive level (`pure`) and only move up as needed. Lucky for us, the Solidity compiler is really good at issuing warnings that let us know when you should use one of these modifiers. ::: #### The Fallback Function The Fallback function is a special type of function available to contracts. As the name suggests, a fallback function is a function that is **called when an otherwise non-existent function is called on the contract.** Here are the rules when it comes to the fallback function: - It must be marked `external`. - It has no name. - It has no arguments. - It cannot return anything. - Only one can be defined per contract. If plain ether is sent to a contract (that is, ether is sent without data), it will be directed to the fallback function. The fallback function must be marked `payable` for the payment to be received; otherwise, an exception will be thrown, and the transaction reverted. ```solidity= contract Test { uint public x ; function() external { x = 1; } } contract Pot { function() external payable { } } contract Caller { function callTest(Test test) public returns (bool) { bool success = address(test).call(abi.encodeWithSignature("nonExistingFunction()")); require(success); // test.x is now 1 address payable testPayable = address(uint160(address(test))); // Sending ether to Test contract, // the transfer will fail, i.e. this returns false here. return (testPayable.send(2 ether)); } function callPot(Pot pot) public returns (bool) { address payable potPayable = address(pot); return (potPayable.send(2 ether)); } } ``` #### Function Modifiers While writing smart contracts, you may find yourself repeating boilerplate code that does stuff like validating inputs or restricting access. Solidity offers a way to reduce boilerplate through **Modifiers,** which is a mechanism to modify the behavior of a function. Modifiers are **code that can be run before and/or after a function call.** We can create a modifier with or without parameters using the `modifer` keyword as such: ```solidity= contract Owner { address public owner; constructor() { // Set the transaction sender as the owner of the contract. owner = msg.sender; } modifier onlyOwner { require(msg.sender == owner); _; // the modified function body will execute here } // This function can only be called by 'owner' function sayHello() public onlyOwner returns (string memory) { return "Hello, world!"; } } ``` Notice the `_` symbol. This tells Solidity to continue executing the rest of the code. In this case, the modified function's body will execute in place of the `_`. Using the `onlyOwner` modifier as an example, the modified function will be executed if and only if the `require` statement passes; otherwise, an exception is thrown. We can also use modifiers to guard against reentrancy attacks: ```solidity= contract ReentrancyContract { address public owner; uint public x = 10; bool public locked; constructor() { // Set the transaction sender as the owner of the contract. owner = msg.sender; } // Modifiers can be called before and / or after a function. // This modifier prevents a function from being called while // it is still executing. modifier noReentrancy() { require(!locked, "No reentrancy"); locked = true; _; // modified function body will execute here // we can also execute code after the modified functions returns! locked = false; } function decrement(uint i) public noReentrancy { x -= i; if (i > 1) { decrement(i - 1); } } } ``` ### Control Flow #### If statements Ah, our old pal: the if-statement. An if-statement is written as follows: ```solidity= contract IfStatement { function knock(uint i) public pure returns (string memory) { if (i == 0) { return "Hey!"; } else if (i == 1) { return "Hi!"; } else { return "Bye!"; } } } ``` #### Loops There are `for`, `while`, and `do... while` loops: ```solidity= contract Loops { function forLoop() public pure { uint i = 0; for (i = 0; i < 10; i++) { // do something... } } function doWhileLoop() public pure { uint i = 0; do { // do something... i++; } while (i < 10); } function whileLoop() public pure { uint i = 0; while (i < 10) { // do something... if (i == 9) { // break and continue work in all these too! break; } else { continue; } i++; } } } ``` ### Mappings Mappings are key-value stores that can be declared as the type: ```solidity= mapping(<KeyType> => <ValueType>) ``` The type of the key can be any built-in type in addition to `bytes` and `string`. No reference type or complex objects are allowed. The type of the value can be any type. Mappings **can only be stored in `storage`** and are typically only used as state variables. They cannot be used as parameters or return parameters of contract functions that are publicly visible. These restrictions are also true for arrays and structs that contain mappings. ```solidity= contract Ledger { mapping(address => uint) public balances; function updateBalance(uint newBalance) public { balances[msg.sender] = newBalance; } } ``` Public mappings, like all other public state variables, have getter functions that are automatically generated. The getter function takes in the key as a parameter and returns the value. ### Events You may have thought to yourself while reading this guide: "where are the print statements?" Good question! Because smart contracts run on the blockchain, outputting information is a little more complicated than printing to stdout. Instead, we log information onto the blockchain using Events. Events are an abstraction on top of the EVM's logging capabilities. They are a mechanism in which your contract can send information to the transaction's log -- a special data structure stored on the blockchain. ```solidity= contract PiggyBank { // Declare an Event event Deposit(address indexed _from, bytes32 indexed _id, uint _value); function deposit(bytes32 _id) public payable { // Emit an event emit Deposit(msg.sender, _id, msg.value); } } ``` :::info Hardhat actually provides the ability to print logging messages and contract variables by calling `console.log()` from your contracts and tests on Hardhat Network. See our **[Testing and Debugging](#Part-4-Testing-and-Debugging)** section for more info. ::: ### Interfaces One of the most powerful features of smart contracts is the ability to call other smart contracts. However, before our contract can talk to another contract on the blockchain that we don't own, we need to define an interface. Say there was a contract on the blockchain that looked like this: ```solidity= contract FavoriteNumber { 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 simple contract allows anyone to store their favorite number and associate it with their Ethereum address. After, anyone could look up that person's favorite number using their address. Now imagine we had another contract that wanted to read the data stored in this contract via `getNum`. To do so, we must first define an interface of the `FavoriteNumber` contract: ```solidity= contract FavoriteNumberInterface { function getNum(address _myAddress) public view returns (uint); } ``` This looks like defining a contract, but there are a few differences: - We're only declaring the functions we want to interact with (in this case, `getNum`), and we don't mention any other functions or state variables. - We don't define any function bodies. Instead of curly braces (`{}`), we simply end the function header with a semi-colon (`;`). It looks kind of like the skeleton of `FavoriteNumber`, which is how the compiler knows it's an interface. By including this interface in our code, our contract knows what the external contract's functions look like, what they take in, and what they return. Now that we have defined our interface, we can use it as such: ```solidity= contract FavoriteNumberInterface { function getNum(address _myAddress) public view returns (uint); } contract MyContract { // The address of the FavoriteNumber contract on Ethereum address FavoriteNumberAddress = 0xabc69... // `favNumberContract` points to the other contract FavoriteNumberInterface favNumberContract = FavoriteNumberInterface(FavoriteNumberAddress); function f() public { // Now we can call `getNum` from that contract: uint num = favNumberContract.getNum(msg.sender); } } ``` Now your contract can interact with any other contract on the Ethereum blockchain, as long as the functions you are calling are marked `public` or `external`. ### Error handling Various functions are provided by Solidity for error handling. Typically, when an error occurs, the state of the contract is reverted back to its original (that is, the state before the contract was called). However, we can also implement checks to prevent unauthorized code access, and to protect against certain conditions: - `assert(bool cond)`: Asserts that a condition must be met. If not, this function causes any state changes to be reverted. This is used for **internal errors**. - `require(bool cond, [string memory message])`: Asserts that a condition must be met. If not, this function causes any state changes to be reverted. This method is to be used for **errors in inputs or external components**. The optional message argument, if provided, allows you to log a custom error message. - `revert([string memory reason])`: When called, this function aborts the contract execution and reverts any state changes. The optional message argument, if provided, allows you to provide a custom message. ```solidity= contract Market { address public seller; modifier onlySeller() { require( msg.sender == seller, "only the seller can call this function" ); _; } function sell(uint amount) public payable onlySeller { if (amount > msg.value / 2 ether) revert("Not enough Ether provided."); // perform the sell operation... } } ``` ## Part 2: Solidity Pitfalls Solidity is infamous for it vulnerablities and eccentricities. In this section, you will learn some (of the many) Solidity pitfalls so you that you can hopefully avoid them in your own code! ### Mappings ```solidity= contract MappingPitfall { mapping(uint256 => uint256) public numToNum; function addToMap(uint256 num) public { numToNum[num] = 1; } function returnKeys() public view returns (uint256[] memory) { return numToNum.keys(); } } ``` This code (specifically `returnKeys()`) won't work. Why is that? That is because `.keys()` is not a function you can call on a mapping. In fact, in Solidity there is essentially **no** way to get the set/list of keys from a mapping. This is because every key-value pair is initialized by default for Solidity mappings (meaning you also can't get a total set of values). ### No Null This also means you will have to be careful of certain appealing mapping usages like - ```solidity= contract MappingPitfall { mapping (address => uint) balances; function checkBalance(address a) public view returns (string memory) { console.log("Checking balance of address", a); console.log("Address exists and balance is", balances[a]); return ""; } } ``` This works, but keep in mind that any address that has not yet been assigned in the mapping will still return a value of 0, as all pairs are initialized with a [default value](https://docs.soliditylang.org/en/v0.8.13/control-structures.html#scoping-and-declarations) and Solidity has no concept of "null". As such, there is no way to tell if a key has been added into a mapping yet. This is a common trend in Solidity, and it is good to remember that any declared variable is auto-intialized as the all zeroes representation of that type. For example: ```solidity= contract NoNullPitfall { uint[5] public arr; function getAtIndex(uint index) public view returns (uint) { return arr[index]; } } ``` As previously discussed, `getIndex(0)` through `getIndex(4)` will return 0, while `getIndex(5)` will error. ### Strings and Looping Strings in Solidity are quite featureless and are unindexible, not concatenate-able, and do not support native ways of finding length. This is partially because of how strings are [stored in Solidity](https://docs.soliditylang.org/en/v0.8.7/internals/layout_in_storage.html#bytes-and-string). In general, **strings in Solidity are one of the most expensive data structures to use**, and should be avoided if possible. Similarly, looping in Solidity is often best avoided due this same potentiality of high gas usage (a category of vulnerability often called "gas griefing"). As such, solutions that involve arrays (also an expensive data structure) are in most cases best repurposed to instead use mappings. ### Reentrancy Attacks We talked about reentrancy attacks briefly during the syntax section, but it's useful to see why reentrancy is such a vulnerability: ```solidity= contract EtherStore { mapping(address => uint256) public balances; function deposit() public payable { balances[msg.sender] += msg.value; } function withdraw() public { uint256 bal = balances[msg.sender]; require(bal > 0); (bool sent, ) = msg.sender.call{value: bal}(""); require(sent, "Failed to send Ether"); balances[msg.sender] = 0; } } ``` Here is a simple contract that allows users to store ether (via the `deposit()` function) as well as withdraw their total balance (via the `widthdraw()` function). While seemingly robust, we can construct an exploitative contract that takes advantage of a mixture of the `fallback()` function as well as reentry to drain this contract of its funds. ```solidity= contract Attack { EtherStore public etherStore; constructor(address _etherStoreAddress) { etherStore = EtherStore(_etherStoreAddress); } fallback() external payable { if (address(etherStore).balance >= 1 ether) { etherStore.withdraw(); } } function attack() external payable { require(msg.value >= 1 ether); etherStore.deposit{value: 1 ether}(); etherStore.withdraw(); } } ``` To initiate this exploit, all that must be done is for `attack()` to be called. This sends 1 ether to `EtherStore` so as to give the sender some balance. At this point, it withdraws that 1 ether. However, on line 12 of the `EtherStore` contract, the transfering back of the balance activates the `Attack` contract's `fallback()` function. This function calls `withdraw()` again, and since `EtherStore` has not yet reached line 14 which resets the sender's balance, the contract still believes the sender has balance and sends 1 more ether back, then again activating the `fallback()` function. This will continue until the contract simply has no more ether to send. While a simple example, we hope that this shows you how small insecurities in Solidity code can be exploited to drastic proportions, as well as demonstrate how important it is to understand the structure and purpose of your contract! :::info **Bonus:** How would you fix this `EtherStore` contract to make it secure against this kind of attack in particular? ::: ## Part 3: The Assignment Now let's see what you've learned! For this assignment, you will design a voting smart contract. Of course, the main problems of electronic voting are how to assign voting rights to voters and how to prevent manipulation, all while preserving the privacy of the voter. We won't solve these problems in this assignment, but we will show how voting can be done, so that vote counting is **automated and transparent**. The high-level idea is this: create one instance of the contract per election, providing a name for each candidate. The creator of the contract, who serves as the election `leader`, will grant voting rights individually to each voter (identified by their address) by calling `registerVoter`. Voters can then cast their vote by calling `castVote`. When you're ready for the results of the election, you can call `winningProposal` to determine which proposal got the most votes. If `winningProposal` is called before any votes have been cast, you should throw an error and revert the transaction. :::success **Task:** In `contracts/Election.sol`, finish implementing the system described above using the provided functions and data structures. **After cloning the stencil, make sure you run `npm install` to install all of the required dependencies.** We have already provided the tests we will use to autograde your assignment in the `tests/` directory. **Look through the test suite to see how we tested the contract and what error messages you must return to pass the tests.** The autograder expects specific error messages when handling certain situations. You can run the test suite locally by running `npx hardhat test`. ::: ## Part 4: Testing and Debugging To run your tests, go into your `test/` directory. We set up `election-test.js` for you, although you are free to write your own tests in the same file. ### Debugging You can print logging messages and contract variables by calling `console.log()` while running contracts and tests on Hardhat Network. To do so, you must first import `hardhat/console.sol` from your contract code: ```solidity= pragma solidity ^0.8.13; import "hardhat/console.sol"; contract LuckyNumber { //... } ``` You can use `console.log` in functions as if you were using it in JavaScript: ```solidity= pragma solidity ^0.8.13; import "hardhat/console.sol"; contract LuckyNumber { function guess(uint _guess) external returns(bool) { console.log("The user guessed: ", _guess); // ... } } ``` and logging output will show when you run your tests! ## Bonus: Non-Fungible Tokens (NFTs) This part of the lab is completely optional, but we thought it would be fun to show you a use of smart contracts that you've almost certainly heard of: NFTs! ### What are NFTs? [Non-Fungible Tokens](https://ethereum.org/en/nft/) (NFTs) are a way to **represent anything unique as an Ethereum-based asset.** They're powered by smart contracts, which means you already know how to make them! An NFT on Ethereum is simply a **smart contract that conforms to the [ERC-721 standard](https://eips.ethereum.org/EIPS/eip-721).** ERC-721 is a standard that defines an API for smart contracts that represent NFTs; it requires the smart contract programmer to implement functions for transferring tokens from one account to another, getting the current token balance of an account, getting the owner of a specific token, and more. Basically, if a smart contract implements all of [these functions and events](https://eips.ethereum.org/EIPS/eip-721#specification), it conforms to ERC-721 standard and is considered an NFT contract. ### What's the difference between tokens and token contracts? A common place of confusion surrounding tokens is mixing up two separate concepts: token contracts and the tokens themselves. **Token contracts** are simply Ethereum smart contracts. Whenever someone "sends" a token, they are really calling a function on a smart contract that's been deployed on the blockchain. In essence, a token contract is a mapping of addresses to balances, alongside some methods to manipulate those balances. These **balances are what represent the tokens themselves**. Someone has a token when their balance in the token contract is not zero. That's all there is to it! What these balances represent is up to the programmer; they could represent money, coins in a game, property, music rights, or voting rights (of course, each of these tokens would be stored in different token contracts). ### Make your own! 1. To get started, create a new Hardhat project using the [setup instructions](#Setup) we covered earlier. 2. Now, move into the contracts folder (`cd contracts`) and create a new file called BoredBluenos.sol (`touch BoredBluenos.sol`). 3. To simplify the implementation of ERC-721, we will use the [OpenZeppelin Contracts library](https://openzeppelin.com/contracts/), which provides battle-tested implementations of the most common ERC standards. Install this library in your project directory by running `npm install @openzeppelin/contracts`. 4. Paste the following code into `BoredBluenos.sol`: ```solidity= pragma solidity ^0.8.0; import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import "@openzeppelin/contracts/utils/Counters.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; contract BoredBluenos is ERC721URIStorage, Ownable { using Counters for Counters.Counter; Counters.Counter private _tokenIds; constructor() ERC721("BoredBluenos", "BLU") {} function mintNFT(address recipient, string memory tokenURI) public onlyOwner returns (uint256) { _tokenIds.increment(); uint256 newItemId = _tokenIds.current(); _mint(recipient, newItemId); _setTokenURI(newItemId, tokenURI); return newItemId; } } ``` So, what exactly are we doing here? Let's break it down: - First, we import three OpenZeppelin smart contract classes: - `@openzeppelin/contracts/token/ERC721/ERC721.sol` contains the implementation of ERC-721. - `@openzeppelin/contracts/utils/Counters.sol` provides counters than can only be incremented/decremented by one. We'll use this to keep track of the total number of tokens we've minted (which will also help us give each token a unique ID). - `@openzeppelin/contracts/access/Ownable.sol` provides [access control](https://docs.openzeppelin.com/contracts/3.x/access-control) to our contract. This ensures only the contract owner can mint tokens. - After our imports, we have our NFT smart contract. You might notice, it's quite small -- it only contains a counter, a constructor, and a single function! Thanks to our inherited OpenZeppelin contracts, the implementation of most of the methods required by ERC-721 is already done for us. - In the ERC-721 constructor, notice we pass in two strings: "BoredBluenos" and "BLU". The first argument is the smart contract's name, and the second is its symbol. These can be whatever you want. - Finally, we have written one function: `mintNFT(address recipient, string memory tokenURI)`. This allows us to mint an NFT! This function takes in two parameters: - `address recipient` is the address that will receive your newly minted NFT. - `string memory tokenURI` is a string that should resolve to a JSON document that describes the NFT's metadata. **The NFT's metadata is crucial since it allows our tokens to have configurable properties like a name, description, image, and more!** You can learn more about configuring the metadata [here](https://docs.opensea.io/docs/metadata-standards). - `mintNFT` then calls some functions from the inherited ERC-721 library and finally returns an integer that represents the ID of the freshly minted NFT. That's it! Obviously, we left out things like configuring the metadata and deploying the contract to the Ethereum mainnet, so we've provided some links that we think could be useful: - [Configuring NFT metadata](https://docs.opensea.io/docs/metadata-standards) - [Deploying smart contracts with Hardhat](https://hardhat.org/guides/deploying.html) - [OpenZeppelin](https://docs.openzeppelin.com/contracts/4.x/) :::info **Did you make an NFT?** Show us on Ed and/or Discord! We'd love to see what you've built ❤️ ::: ## Helpful things * [Solidity Documentation](https://docs.soliditylang.org/en/v0.8.12/) * [Solidity By Example](https://solidity-by-example.org/) * [Hardhat Documentation](https://hardhat.org/getting-started/) * [Testing with Hardhat](https://hardhat.org/tutorial/testing-contracts.html) * [Remix Documentation](https://remix-ide.readthedocs.io/en/latest/)