Muchang Bahng

Ethereum


Contents
  1. Solidity
  2. Basic Topics
  3. Intermediate Topics

Solidity

[Hide]

A prerequisite to this section is the Blockchain section. Recall that a wallet is really just some application that contains a hierarhical structure of private keys. These private keys in turn generate public keys, which generate addresses (at least in the Bitcoin protocol). Things may slightly differ in the Ethereum protocol, but this is the general relationship. An address is just some hash that identifies some entity owning whatever keys that are used to gain access to UTXOs in the blockchain. A smart contract is also represented by a hash, but the owner of it is a program, rather than an individual/party. Sending ether to this smart contract address allows you to run the program, which have several applications mentioned later.

We will use the web browser-based IDE remix.ethereum.org. We should always write down the version of solidity we are using at the top of each .sol file. The version is important for proper compilation.
pragma solidity 0.6.0     // Version 0.6.0 
pragma solidity ^0.6.0    // Version 0.6.Anything 
Furthermore, Solidity strongly recommends us to include the license of our program at the header. The following MIT license indicates that our code is open source.
// SPDX-License-Identifier: MIT
Furthermore, remember that 1 Ether is worth: Finally, know that gas is pretty much the amount of ether you have to pay (usually demoniated in Gwei) to execute a contract and its functions (at least the non-view/pure ones). Every time you execute a function, you're using someone else's computer resources to run the Ethereum Virtual Machine, so due to naturally supply and demand, the existence of this fee is natural.
Basic Types and Contracts
We introduce the basic types in Solidity. Since Solidity is a statically-typed language, the type of each variable should be specified. For now, do not worry about the visibility modifiers like public, private, external, and internal.
  • The signed integer int and unsigned integer uint are by default stored in 256-bits. We can, however, specify their number of bits accordingly as below. It is recommended to keep it 256 for compatibility reasons between smart contracts. If an integer has a greater value than the maximum possible, it reverts back to the smallest, making integer addition really a modulo addition.
        uint public myInt = 32;       // unsigned integer 
        int8 external myInt2 = -3;    // signed integer of 8-bits
        // uint8 max size: 255 
        // uint16 max size: 65,535 
        // uint32 max size: 4,294,967,295
        
  • The bool is pretty self-explanatory.
        bool internal myBool = false;     // Boolean 
        
  • The address type allows you to store addresses in hexadecimal.
        address private EthWallet = 0xA0362AD63a5ac9e630849f19709e46368b9610Ab; 
        
  • The string type is also pretty self-explanatory.
        string public myStr = "Hello World"; 
        
  • The bytes type is quite new and confusing, especially since it seems to cover the same things as strings. However, they store data in bytes rathen than UTF-8 data. As a rule of thumb, use bytes for arbitrary-length raw byte data and string for arbitrary string (UTF-8) data. They have the advantage of using less gas than strings. You can limit the length to a certain number of bytes from bytes1 to bytes32 if possible.
        bytes32 myCat = "cat";      // 32-byte data 
        
  • Note that there are no floating point numbers in Solidity! They may be innaccurate, which is extremely risky when dealing with finances.
To cast one type to another, we can simply just use the syntax below:
uint256 tooBig = 250;             // too many bits for a small number 
uint8 justRight = uint8(tooBig)   // casted it into a smaller-bit uint 
Solidity contracts are like a class in any other object-oriented language, which contain data as state variables and functions which can modify these variables. They can be created with the contract keyword. The state variables, which contain the data representing the state of the contract, are shown below, and we use the default constructor function, which gets called as soon as the contract is deployed.
contract SimpleStorage {
    uint256 myInt = 0;          // state variable
    int private myAge = 21;     // state variable 

    constructor() {}            // default constructor 
}
Functions, Scope, View/Pure
The syntax of functions generally looks like the following. It is called with a function keyword, followed by the function_name and its parameters.
function function_name(parameter_list) scope returns(return_type) {
    // block of code
}
  • The type of the parameters can be determined by function_name(uint a, uint b).
  • The return value type can be specified with the returns keyword. Multiple return types may be specified.
  • The type of function determines what the function can do to your contract.
    • View functions do not modify the state variables but they do read them, which ensures that state variables cannot be modified after calling them. You do not have to make a transaction to call a view function, since we are not making a state change to the contract and therefore to the blockchain (in Remix, public view functions are represented with blue buttons).
    • Pure functions do not read nor modify the state variables. It can only use local variables that are declared in the function and the arguments that are passed to the function. You do not have to make a transaction to call a pure function, since we are not making a state change to the contract and therefore to the blockchain. These functions are usually used to perform math calculations.
    • Functions that are not specified to be view or pure by default have access to both read and modify powers. You do have to make a transaction to call these functions.
  • The scope, or visibility, of these functions determine who has access to the functions and state variables in your contract and how they interact with them. For cohesiveness, we can just think of state variables as as function calls that looks at a variable and returns its value, making it also a (view) function. Functions have to be specified by any of the four visibilities, but for state variables external is not allowed:
    • External functions can only be called from outside the contract in which they were declared.
    • Public functions and variables can be accessed by all parties within and outside the contract. When the visibility is not specified, the default visibility of a function is public.
    • Internal functions and variables are only accessible within the contract in which they were declared, although they can be accessed from derived contracts. When visibility is not specified, state variables have the default value of internal.
    • Private functions are only accessible within the contract in which they were declared. Private functions are also the only functions that cannot be inherited by other functions.
Finally, know that just like other languages, every variable is either defined in the local scope (within the function only), the contract scope (as a state variable within the contract), or the global scope (i.e. variables representing the state of the entire blockchain).
Contracts & Functions
These all sound familiar to the properties of other languages, but a new characteristic is the visibility of state variables and functions, which controls who has access to the functions and state variables in your contract and how they interact with them. For cohesiveness, we can just think of state variables as as function calls that looks at a variable and returns its value, making it also a function. More specifically, it becomes a view function. Functions have to be specified by any of the four visibilities, but for state variables external is not allowed:
  • External functions can only be called from outside the contract in which they were declared. They cannot be called from within the contract. In order to do this we must set the visibility to public.
  • Public functions and variables can be accessed by all parties within and outside the contract. When the visibility is not specified, the default visibility of a function is public.
  • Internal functions and variables are only accessible within the contract in which they were declared, although they can be accessed from derived contracts. When visibility is not specified, state variables have the default value of internal.
  • Private functions are only accessible within the contract in which they were declared. Private functions are also the only functions that cannot be inherited by other functions.
Now let's dive into building contracts a bit further. Let's build a simple contract that stores a number. Note that like in other languages, it is conventional to precede local variable names with an underscore.
pragma solidity ^0.6.0 

contract SimpleStorage {
    
    // this will get initialized to 0 
    uint256 favoriteNumber; 

    function store(uint256 _favoriteNumber) public {
        favoriteNumber = _favoriteNumber   
    }
}
Note that the state variable of this contract is specified by favoriteNumber. Since the function store is public, we can interact with it from outside the contract, and calling the function with an int256 parameter updates the state variable of it. When we deploy this contract, it becomes a part of the blockchain, and modifying the state variable of this contact means to modify the blockchain itself. Therefore, transactions, smart-contract interactions, and function calls can be used interchangeable (kinda), because whenever you call a function (or whenever you make some state change to the blockchain), you're also making a transaction, which will cost a bit of gas. That is why whenever we call some function, we must always pay a bit of gas fees.

However, notice that even though we can call the store function and update the state variable, we can't actually look at what the value of favoriteNumber is. To do this, we can simply set the visibility of the state variable to public, changing the line to uint256 public favoriteNumber;. Furthermore, let us introduce a public view function retrieve that returns the value of favoriteNumber and a pure function add that simply adds the two together (but does not set it as the new state variable). Note that the pure function does not change the state variable of the contract.
pragma solidity ^0.6.0 

contract SimpleStorage {
    
    // this will get initialized to 0 
    uint256 public favoriteNumber; 

    function store(uint256 _favoriteNumber) public {
        favoriteNumber = _favoriteNumber   
    }

    // view, pure 
    function retrieve() public view returns(uint256) {
        return favoriteNumber; 
    }

    function add(uint256 favoriteNumber) public pure {
      uint256 twoTimes = favoriteNumber + favoriteNumber; 
      return twoTimes; 
  }
}
Now if we remove the public keyword from the state variable initialization, it gets set back to internal. But this is no problem, since we can call the public retrieve function to return the value.
Error Handling: Require, Assert,
We can use three statements to handle errors and to check the truth of something.
  • The require statement checks whether the statement within the parenthesis is true and stops the contract if it is false, providing an error statement and refunding the gas.
        function divide(int _num1, int _num2) public pure returns(int) {
          require(_num2 != 0, "2nd number can't be zero!"); 
          int quotient = _num1 / _num2; 
          return quotient; 
        }
        
  • The assert statement checks for a statement that should never be false. If the statement is false, then it reverts all state changes to the blockchain and uses up gas.
        function divide(int _num1, int _num2) public pure returns(int) {
          assert(_num2 > 0); 
          int quotient = _num1 / _num2; 
          return quotient; 
        }
        
  • The revert only sends a message and must be put inside an conditional if statement.
        function divide(int _num1, int _num2) public pure returns(int) {
          
          if(_num2 < 0) {
              revert("2nd number must be greater than 0")
          }
          int quotient = _num1 / _num2; 
          return quotient; 
        }
        
Global Variables, Generating Random Numbers
Let us list some useful global variables (describing the state of the blockchain) that may be useful in future calculations. The complete list can be found here.
  1. block.basefee gives the current block's base fee in uint.
  2. block.chainid gives the current chain id in uint.
  3. block.coinbase gives the current block miner's address in address payable
  4. block.number gives the current block number in uint.
  5. block.timestamp gives the current block timestamp in seconds since Unix epoch in uint.
  6. msg.data gives the complete calldata in bytes.
  7. msg.sig gives the first four bytes of the calldata in bytes4.
  8. msg.value gives the number of wei send with the message in uint.
  9. msg.sender gives the sender of the message in address.
We can utilize these global variables to create a random integer generator (within a certain range). True randomness it impossible, but we can have some sort of random seed to generate from. We take the following steps:
  1. We call the global block.timestamp which should give a different uint every second.
  2. We input this uint to the global variable/function abi.encodePacked, which returns a bytes type. This function performs packed encoding of the given arguments (note that this encoding can be ambiguous).
  3. We input this bytes to the keccack256 hash function, which outputs a 256-bit output in the form of a bytes32 type.
  4. We cast this into a uint type, giving it the impression of randomness and modulo it by _max.
The full code is shown below:
contract demonstrate {
    function getRandNum(uint _max) public view returns(uint) {
        uint rand = uint(keccak256(abi.encodePacked(block.timestamp))); 
        return rand % _max; 
    }
}
Memory vs Storage: Strings, Bytes, Arrays
There is a bit of difficulty when dealing with strings, bytes, and arrays. They are all dynamic arrays (elements of strings are characters, elements of bytes are single bytes). But apparently we have to do memory allocation for them, and so rather than writing function parameters like this: string _str1, we must write them with the proper memory allocation, one of two ways:
  • string memory _str1: The memory keyword means that the memory for this string will be deleted as soon as the function is finished executing.
  • string storage _str1: The storage keyword means that the memory for this string will persist even after the function execution.
Just know that memory refers to short-term (like RAM) storage that gets deleted after execution of the function, while storage persists after execution (like drives). Furthermore, the regular built-in functionalities of string such as concatenation, determining the string's length, and reading/changing a character are not built-in within Solidity's string class! We can make a function to concatenate strings using our global variables again:
function combineString(string memory _str1, string memory _str2) public pure returns(string memory) {
    return string(abi.encodePacked(_str1, " ", _str2));       // Concatenates _str1 and _str2 with a space in between
}
Here is a function returning the number of characters within a string:
function numChars(string memory _str1) public pure returns(uint) {
    bytes memory _byte1 = bytes(_str1); 
    return _byte1.length; 
}
Structs, Arrays, Mappings
Three more often-used types are structs, arrays, and mappings.
  • The syntax for arrays are quite different. To declare an array of fixed size in Solidity, you must specify the type of the elements and the number of elements required by an array as follows. Arrays can have a fixed size or be dynamically sized.
        type[ arraySize ] arrayName       # fixed size 
        type[] arrayName                  # dynamically sized 
        
    We can append a last element, delete the last element, get the length, set the element at a certain index to 0, delete a certain index of an array, sum the elements of an array, and more...
        contract demonstrate { 
    
            uint[] arr1;            // create dynamically-sized array
            uint[10] arr2;          // create statically-sized array of 10 elements
            uint [] public numList = [1, 2, 3, 4, 5]; 
        
            function addToArray(uint num) public {
                // push to array (last element) 
                arr1.push(num)
            }
        
            function removeFromArray() public {
                // pop array (delete last element) 
                arr1.pop(); 
            }
        
            function getLength() public view returns (uint) {
                // return length of array
                return arr1.length; 
            }
        
            function setIndexToZero(uint _index) public {
                // the delete function just sets the value at the index to 0
                delete arr1[_index]; 
            }
    
            function removeIndex(uint _index) public {
                // removes the element at specified index 
                for (uint i = _index; i < arr1.length-1; i++) {
                    arr1[i] = arr1[i+1]; 
                }
                arr1.pop(); 
            }
    
            function getArrayVals() public view returns(uint[] memory) {
                // needed since the arr1 state variable is not public 
                return arr1; 
            }
    
            function sumNums() public view returns(uint) {
                // sums all elements of an array  
                uint _sum = 0; 
                for (uint i = 0; i <= numList.length-1; i++) {
                    _sum += numList[i]; 
                }
                return _sum; 
            }
    
            function sumNums2() public view returns(uint) {
                // sums all elements of an array, but using a while loop 
                uint _i = 0; 
                uint _sum = 0; 
                while (_i < numList.length) {
                    _sum += numList[_i]; 
                    _i++;
                }
                return _sum; 
            }
        
        }
        
  • We can think of a struct as a custom type with certain attributes/fields, similar to structs in Julia.
        contract test {
            struct Book {
                string title; 
                string author; 
                uint book_id; 
            }
        
            // Initialize a public Book object called 'book'
            Book public book = Book('Learn Java', 'TP', 1); 
        }
        
    Let us create a contract that implements a struct, with functions that adds structs to arrays and gets from them too.
        contract demonstrate {
            struct Customer {
                string name;
                string custAddress;
                uint age;
            }
    
            Customer[] public customers;
    
            function addCust(string memory n, string memory ca, uint a) public {
                // adds Customer object to to customers array 
                customers.push(Customer(n, ca, a));
            }
    
            function getCust(uint _index) public view returns (string memory n, string memory ca, uint a){
                // returns Customer object by index from customers array 
                Customer storage cust = customers[_index];
                return (cust.name, cust.custAddress, cust.age);
            }
          } 
        
  • A mapping is similar to a Python dictionary, which have some key-value pair.
        mapping(key => value) mapping_name;     // Initialization of mapping 
        mapping_name[key] = value               // Adding key-value to mapping
        
    Let us create a contract implementing a mapping that assigns a superhero name with the real name.
        contract demonstrate {
            mapping(string => string) public myMap;
    
            function addHero(string memory _secret, string memory _name) public {
                // adds hero name and real name to myMap 
                myMap[_secret] = _name;
            }
    
            function getName(string memory _secret) public view returns(string memory){
                // returns the real name given the hero name from myMap
                return myMap[_secret];
            }
        
            function deleteName(string memory _secret) public {
                // deletes the hero/real name pair 
                delete myMap[_secret];
            }
        }
        
    We can also predefine a struct and implement a contract that takes in a mapping that maps an index to a Customer object.
        contract demonstrate {
    
            mapping(uint => Customer) customer;
    
            function addCust2(uint custID, string memory n, string memory ca, uint a) public {
                // Map customer data to a index
                customer[custID] = Customer(n, ca, a);
            }
    
            function getCust2(uint _index) public view returns (string memory n, string memory ca, uint a) {   
                // Retrieve customer data using an index 
                return (customer[_index].name, customer[_index].custAddress, customer[_index].age);
            }
    
        }
        
We can also create nested mappings. If we wanted a customer list (mapping indices to Customer objects) for multiple businesses, we would need a map that first took in a business and then outputted another map where we can find the customers by index.
contract demonstrate {
    mapping(address => mapping(uint => Customer)) public myCusts;     // initialize iterated mapping 

    function addMyCusts(uint custID, string memory n, string memory ca, uint a) public {
        // Adds index-Customer data for each person calling function
        // Now has data on which address sent which data 
        myCusts[msg.sender][custID] = Customer(n, ca, a);     // msg.sender is global variable returning address of person calling contract 
    }
}
Finally, we can create a ledger with a smart contract. The following creates a map from addresses to uint, where the values can be changed or returned.
contract MyLedger {
    // Create a map of addresses and balances
    mapping(address => uint) public balances;

    // Change the balance for the address
    function changeBalance(uint newBal) public {
        // msg.sender is the sender of the message
        balances[msg.sender] = newBal;
    }

    // Get current balance for address
    function getBalance() public view returns (uint){
        return balances[msg.sender];
    }
}
For, While, Do-While Loops & Conditionals
A while loop has the syntax
while (condition) {
  ...
}
Implementing it in a contract looks like this, which, upon calling the loop() function, pushes 5 elements into the data array.
contract demonstrate {
    uint[] public data; 

    uint8 j = 0; 

    function loop() public returns (uint[] memory) {
      while (j < 5) { 
        j++; 
        data.push(j); 
      }
      return data; 
    }
}
The do-while loop is very similar to the while loop, but it checks the condition at the end of the loop. So, the loop will execute at least one time even if the condition is false.
do {
  ...
} while (condition) 
We can implement it in a contract as such:
contract demonstrate {
  uint[] public data; 

  uint8 j = 0; 

  function loop() public returns (uint[] memory) {
    do { 
      j++; 
      data.push(j); 
    } while (j < 5); 
    return data; 
  }
  
}
A for loop has syntax similar to that of JavaScript.
for (initialization; test condition; iteration statement) {
  ... 
}
It is implemented in a contract as such:
contract test {
  uint[] public data; 

  uint8 j = 0; 

  function loop() public returns (uint[] memory) {
    for (uint i = 0; i < 5; i++) { 
      j++; 
      data.push(j); 
    } 
    return data; 
  }
}
We can implement conditionals in a contract. In this example, we will create a function that returns what school level you should be going to depending on your age.
contract demonstrate {

    string myName = "Muchang"; 
    uint age = 8; 

    function whatSchool() public view returns(string memory) {
        if (age < 5) {
            return "Stay home"; 
        } else if (age == 5) {
            return "Go to Kindergarten";
        } else if (age >= 6 && age <= 17) {
            uint _grade = age - 5; 
            string memory _gradeStr = Strings.toString(_grade)      // convert uint to string
            return string(abi.encodePacked("Grade ", _gradeStr)); 
        } else {
            return "Go to college"; 
        }
    }
}
Date & Time
Solidity has time units with the lowest unit at 1 second. We demonstrate this by running the code below. Calling the function after contract deployment shouldn't lead to an error since none of the lines are false.
contract demonstrate {
    function timeUnits() public pure {
        // If any of these aren't true the function throws
        // an error
        assert(1 seconds == 1);
        assert(1 minutes == 60 seconds);
        assert(1 hours == 60 minutes);
        assert(1 days == 24 hours);
        assert(1 weeks == 7 days);
    }
}
Constructors and Inheritance
If we would like to specify the state variables of a contract upon deployment, we can use the constructor function for that. Let us define a Shape contract with state variables height and width. We can set their values upon initialization as such, similar to how we would do it in other OOP languages.
contract Shape {
    uint height; 
    uint width; 

    constructor(uint _height, uint _width) {
        height = _height; 
        width = _width; 
    }
}
To construct a child contract that inherits from a parent contract, we use the is keyword. In here, we construct a child class Square and Rectangle.
contract Square is Shape {
    constructor(uint s) Shape(s, s) {}

    function getArea() public view returns(uint) {
        return s**2; 
    }
}

contract Rectangle is Shape {
    constructor(uint h, uint w) Shape(h, w) {}

    function getArea() public view returns(uint) {
        return h*w 
    }
}
Modifiers: Owner Permissions
Function modifiers allow you to block the execution of certain functions unless the address of the caller is the owner. We first create an Owner contract which first identifies the owner address and immediately saves it as msg.sender within the constructor. Then, we create the modifier with the require statement that whoever sent the message must be the owner. The main use for modifiers is for automatically checking a condition prior to executing a function. If the function does not meet the modifier requirement, an error is thrown. The syntax for one kind of modifier is
modifier MyModifier {
    require(msg.sender == owner); 
    _;
}
The symbol _; is called a merge wildcard, and it must always be present in the modifier. It merges the function code with the modifier code where the _; is placed. In other terms, the body of the function (to which the modifier is attached to) will be inserted where the special symbol _; appears in the modifier’s definition.
contract Owner {
    address owner; 

    constructor() public {
        owner = msg.sender; 
    }

    // If caller is owner then continue executing the function that uses this modifier
    modifier onlyOwner {
        require(msg.sender == owner); 
        _;      // a merge wildcard
    }
}
We can place whatever restricted-access function within this contract, but we will create a child contract for our purposes. Let us create a subcontract with two state variables, a mapping of purchasers and a price of some good. Clearly, the deployer of the contract, who is made the owner (from the constructor of Owner) sets the price upon deployment. If an individual would like to express the intent to purchase this good for this price, they can call the public payable function purchase which adds their address (msg.sender) and their intent to buy (true) in the purchasers mapping. The function setPrice can change the price, but because of the onlyOwner modifier, the conditions mentioned in the modifier above are immediately placed, restricting access to this function to everybody except for owner.
contract Purchase is Owner {
    // Mapping that links addresses for purchasers
    mapping (address => bool) purchasers;

    uint price;

    constructor(uint _price) {
        price = _price;
    }

    // You can call this function along with some ether because of payable
    function purchase() public payable {
        purchasers[msg.sender] = true;
    }

    // Only the owner can change this price
    function setPrice(uint _price) public onlyOwner {
        price = _price;
    }
}
Making Contracts for Funding and Withdrawing
Now let us create a contract that can store and send funds. Note a few things: Now, let us create a simple smart contract where we can deposit and withdraw ether from.
contract EtherWallet {
    address payable public owner;        // initialize address payable state variable

    constructor() {
        owner = payable(msg.sender);     // set sender address to owner variable upon deployment
    }

    function deposit() public payable {} // function that can receive ETH payments

    modifier onlyOwner {
        require(owner == msg.sender); 
        _;
    }

    function withdraw(uint _amount) public onlyOwner {
        (bool success, ) = owner.call{value: _amount}(""); 
        require(success, "Failure: Not Sent");
    }

    function withdrawAll() public onlyOwner {
        (bool success,) = owner.call{value: address(this).balance}(""); 
        require(success, "Failure: Not Sent");
    }

    function getBalance() external view returns (uint) {
        return address(this).balance; 
    }
}
Here is the address of the wallet. Just for reference, you can also use the transfer function, which has less confusing syntax:
function withdraw(uint _amount) public {
    payable(msg.sender).transfer(_amount); 
}
Fallback Functions

TBD

[Hide]