Ethereum 101: Part 2 - Smart Contracts



In the previous part of this series, I discussed “contract accounts” in Ethereum. Contract accounts contain smart contracts. In this article, I will go into more details of what smart contracts are and will look at some code examples to see how they work. Many sections of this article have been influenced by the Mastering Ethereum book, which is an excellent reference book on Ethereum.


The term “smart contract” was first introduced by Nick Szabo. Mr. Szabo used a vending machine as an analogy to explain smart contracts. The vending machine operates with certain rules. If a user selects an item and pays the displayed price then the vending machine will output the selected item.


The basic idea of smart contracts is that many kinds of contractual clauses (such as liens, bonding, delineation of property rights, etc.) can be embedded in the hardware and software we deal with, in such a way as to make breach of contract expensive (if desired, sometimes prohibitively so) for the breacher. A canonical real-life example, which we might consider to be the primitive ancestor of smart contracts, is the humble vending machine. Within a limited amount of potential loss (the amount in the till should be less than the cost of breaching the mechanism), the machine takes in coins, and via a simple mechanism, which makes a beginner's level problem in design with finite automata, dispense change and product fairly. - Nick Szabo

Smart contracts in Ethereum are essentially code that can perform a state change (see here for more details on state change). For e.g. a NFT seller can create a smart contract in which he specifies the amount of ETH that he would like to receive from a buyer to transfer the ownership of the NFT. An interested buyer can send the appropriate amount of ETH to execute the smart contract. Upon execution, the NFT ownership will be updated to reflect that that NFT belongs to the buyer. The ETH that was sent by the buyer will be deposited in the seller's Ethereum account. Therefore, a change in the “world state” has been implemented by this smart contract. The above transaction can also be executed without a smart contract. For e.g. a potential buyer can send the ETH directly to the seller and the seller can send the NFT to the buyer after confirming receipt of ETH. However, this introduces trust assumption in the system as the buyer will need to trust the seller to send the NFT upon receiving the ETH. Smart contract gets around this challenge by allowing such transactions to happen in a trust-less manner.


Properties of smart contracts


Smart contracts have the following properties:


Property #1: Smart contracts are transparent:

Once a smart contract is created, the code of the smart contract is available on the blockchain and any user can read and verify the code. For e.g. Uniswap is a decentralized exchange and has a smart contract that can be used by any user to create a liquidity pool for an asset pair. A liquidity pool is a collection of crypto assets and users can "trade" with this pool and swap crypto assets (i.e. exchange their assets). From Uniswap’s website:


Every Uniswap pool is a unique instance of the UniswapV3Pool contract and is deployed at its own unique address. The contract source code of the pool will be auto-verified on etherscan. For example, here is the ETH/USDC 0.3% pool on Ethereum mainnet: https://etherscan.io/address/0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8

This contract has been deployed at the following address: 0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8. Any user can read the smart contract contained at this address by visiting etherscan as shown in exhibit 1.


Property #2: Smart contracts are Permissionless:

Anyone can interact with a smart contract. User just needs to have an EOA (Externally owned account) to talk to a smart contract. Users do not have to provide any KYC (Know Your Customer) information like driving license, bank account statements etc for creating an EOA. Therefore, users cannot be censured by a private company/government from creating an EOA and, thereby, from interacting with smart contract


Property #3: Smart contracts are Deterministic:

A Smart contract is just code. This code will always run in a predictable fashion. The execution will not change based on the user i.e. the code execution result will be the same for all users. Therefore, smart contracts do not discriminate among the users. The code may be faulty but it will be faulty for everyone.


Property #4: Smart contracts are Autonomous:

Once deployed, a smart contract does not require permission from its creator before execution. Therefore, any user can interact with a smart contract that has been created and deployed on the blockchain without any explicit permission from its creator.


Property #5: Smart contracts are Permanent:

Smart contracts cannot be changed once deployed. They can be deleted but only if they have the “delete” function included in it at the time of creation. Therefore, any user can read the code and determine if the code has a delete function. If it does not then they can confidently assume that the smart contract will exist forever. For example: Uniswap has deployed various versions of its contract. At the time of writing, the latest version of Uniswap deployed is the v3 version. However, Uniswap’s v2 contract version continues to see some activity. This is because even though Uniswap is the creator of the v2 contract, Uniswap cannot delete the v2 contract and force users to migrate from v2 to v3 contracts. Exhibit 2 shows the activity in the v2 and v3 USDC/ETH pool for Uniswap.



Lifecycle of a smart contract:


Once a contract has been created, it has to be registered. Registering a smart contract on the blockchain requires a special transaction whose destination is the address 0x000…., which is also known as the zero address. The zero address is a special address that tells the Ethereum blockchain that you want to register a contract. As per property #3 of smart contracts, they are deterministic and the contract creator does not get any specialized treatment by the contract. However, the contract creator can code in special privileges into the contract. However, as per property #1, any such special privileges will be visible by all users of the smart contract and that may reduce the trust that they have in the smart contract. Note that contract accounts do not have a private key. Therefore, the contract creator does not own the smart contract as there is no private key for the contract that can be owned by anyone.


Once created, contracts can be executed. Contracts never run on their own or in the background. They lie dormant until a transaction triggers execution. The execution can be directly triggered by a transaction that originates from an EOA or by a message that is triggered by another contract (which can in turn be triggered by another contract or by a transaction from an EOA).


Transactions are atomic. Regardless of how many downstream contracts are called by a contract or what those contracts do when called, if execution fails due to an error then all state changes are rolled back. Note that the transaction originator has to still pay for the gas that was consumed for running a contract even if it fails and if the state change has been reversed.


Contract code cannot be changed. However, it can be deleted, removing the code and its internal state/storage from its address, leaving a blank account. Any transactions sent to that account address after the contract has been deleted will not result in any code execution. To delete a contract, you execute an EVM opcode called SELFDESTRUCT (previously called SUICIDE). This operation costs negative gas, a gas refund, thereby, incentivizing the contract creator to free up the blockchain resources by deleting contracts that are no longer needed. The SELFDESTRUCT functionality will only be available if the contract author programmed the smart contract to have that functionality. If the contract code does not have a SELFDESTRUCT opcode then the smart contract cannot be deleted. As mentioned above, users can read the contract code and identify if the "SELFDESTRUCT" functionality has been included in the contract code and that will inform them the amount of control that the contract creator has on the contract.


Real example of Smart contract code:

Smart contracts can be written in different programming languages. The 2 most common programming languages for smart contract development are: (1) Vyper and (2) Solidity. In the rest of the article, we will focus on Solidity.


Solidity programs are just plain text files and anyone can write a smart contract by using a text editor and saving the text file with a .sol extension. The user will then need to use a Solidity compiler to convert their .sol file/program into “bytecode”. Bytecode is the instruction that can be understood by a computer. In this case, bytecode is the instruction that can be understood by the Ethereum system (Ethereum Virtual Machine). Therefore, it is necessary to translate the program into bytecode so that it can be deployed on Ethereum blockchain.


Mastering Ethereum has the following code snippet and I will be using that as an example. I will go through the code line by line. This code is available here. This code lets users request an amount of ETH that then want to access and the faucet will supply the ETH to them.

1    // Version of Solidity compiler this program was written for
2    pragma solidity 0.6.4;
3    // Our first contract is a faucet!
4    contract Faucet {
5    // Give out ether to anyone who asks
6    function withdraw(uint withdraw_amount) public {
7      // Limit withdrawal amount
8      require(withdraw_amount <= 100000000000000000);
9      // Send the amount to the address that requested it
10     msg.sender.transfer(withdraw_amount);
11    }
12   // Accept any incoming amount
13   receive() external payable {}
14   }

Line 1: Solidity programs support comment statements in the code. Comment statements lead to better readability of the code but are not executed. The first line in the above program is a comment statement and is skipped during code execution


Line 2: Solidity programs have to be compiled by a compiler. A compiler is a piece of software that converts the above "human readable" code into bytecode, which can be understood by the computer (i.e. the Ethereum virtual machine). The language is constantly evolving and some features of the code may be compatible with only a specific version of the Solidity compiler. Therefore, line 2 specifies that the program should be compiled with a Solidity compiler version that is at least 0.6.4 or above. At the time of writing, the latest version of Solidity is 0.8.15


Line 3: Another comment statement


Line 4: This line declares a contract object. Solidity is an object oriented programming language. As per Solidity's official documentation:

A contract in the sense of Solidity is a collection of code (its functions) and data (its state) that resides at a specific address on the Ethereum blockchain

Therefore, this line marks the start of a new contract named "Faucet". Everything between the curly bracket at the end of this line and the end curly bracket on line 14 is included in the scope/definition of this "Faucet" contract. We will go into more details of object later in this article


Line 5: Another comment statement


Line 6: In this line, a new function, which is part of the Faucet contract, is being declared. A function is an operation or set of instructions that can act upon the supplied inputs. For e.g. in Microsoft excel, sum() is a function that adds and outputs the sum of all numbers that are passed to it as arguments or inputs.


In line 6, a function - "withdraw" - is being declared. The scope of this function (i.e. the operation that it does) is defined between the curly bracket at the end of this line and the curly bracket on line 11.


The "withdraw" function has an input/argument that has been declared as "uint withdraw_amount". The name of the input variable is "withdraw_amount". The "uint" in front of this variable name indicates that this function expects the input to be an (unsigned) integer. For e.g. the square function expects the input to be a number. If a user tries to run the square function on a string (i.e. alphabets) then the function will not work. Similarly, the "withdraw" function expects and will run only if the supplied input in an integer. Unsigned integer means that the number has to be a positive integer. This makes sense as the function wants to let users withdraw an amount of ETH from the contract and withdrawing a negative amount of ETH will not make any sense.


The function is declared as a "public" function. This means that the function can be called by other contracts as well. As mentioned above, a contract code cannot run on its own but can only be triggered by a (1) transaction originating from an EOA or (2) message from another contract (which in turn has been triggered by a transaction from an EOA). Therefore, marking this function as "public" means that another contract can trigger this function as well.


Line 7: Another comment statement


Line 8: In this line, the "withdraw" function is calling another function named require(). The require() function is a pre-defined function in Solidity i.e. this function is available for all other functions to use. This is similar to sqrt() function or sum() function in Microsoft excel as these functions are also available from the get-go to be used in an Excel spreadsheet as part of other formulas. The input of the require() function is a logical test. In this code, the require() function will test if the withdraw_amount that has been inputted into the main withdraw() function is less than or equal to 100000000000000000. The default unit in Ethereum is wei, which is a sub unit of Ether. Therefore, the require() function will check if the user is trying to withdraw funds less than or equal to 100000000000000000 wei (= 0.1 ETH). If the condition is true then the require() function will let the rest of the function code to be executed. If the condition is false then the require() function will terminate/stop the execution. Therefore, for the require() function:

  • Input = logical condition/test

  • Output:

  • Continue execution if input is TRUE

  • Stop execution if input is FALSE

Line 9: Another comment statement


Line 10: "msg" refers to the message that triggered the withdraw() function. Sender refers to an attribute of the "msg" object. Specifically it refers to the sender address of the transaction i.e. the address of the EOA or the contract that triggered the the withdraw() function. transfer() function is another pre-defined function. In this case, the transfer() function transfers the amount specified as the input to the function from the current Ethereum account (i.e. the contract account) to the account of the sender i.e. the EOA account or the contract account that triggered the call


Line 11: Curly bracket that ends the definition of the withdrawn() function


Line 12: Another comment statement


Line 13: receive() function is another pre-defined function. The function is being declared with 2 additional descriptors - external and payable. For the time being, we can assume that "external" is the same as public i.e. another contract can trigger this function. Payable means that this function can accept incoming payments/transfers. This function is a "fallback" function. This means that if the transaction that triggered the contract didn't name any of the declared function (which is only the withdraw() function in the case of the Faucet contract) or any function or didn't contain data then the fallback function will be executed


Line 14: Curly bracket that ends the definition of the Faucet contract code


Another example of sample contract code with line by line explanation can be found here.


Deep dive into Solidity programming:


#1 Pre-defined variables and methods/functions:

When a contract is executed in the EVM, it has access to a small set of global objects and pre-defined functions i.e. it has access to some properties/data related to the user (more specifically, user's Ethereum account) that triggered the transaction and some commonly used operations.


A brief explanation of objects is available here:


Real-world objects share two characteristics: They all have state and behavior. Dogs have state (name, color, breed, hungry) and behavior (barking, fetching, wagging tail). Bicycles also have state (current gear, current pedal cadence, current speed) and behavior (changing gear, changing pedal cadence, applying brakes). Software objects are conceptually similar to real-world objects: they too consist of state and related behavior. An object stores its state in fields (variables in some programming languages) and exposes its behavior through methods (functions in some programming languages). Methods operate on an object's internal state and serve as the primary mechanism for object-to-object communication.

Therefore, software objects can be considered as a collection of data and functions.


Transactions/message call context:

Solidity has a "msg" object. The "msg" object refers to the call that triggered a contract. Therefore, the code inside contract A can reference some of the data in the call that triggered contract A. The call can be either a transaction (triggered by an EOA) or a message call (triggered by another contract). Some attributes that are exposed by the "msg" object are as follows:

  • msg.sender: represents the address that initiated the contract call. If the contract was initiated by an EOA then this attribute will return the address of the EOA. If the contract was initiated by another contract then this will return the address of the other contract

  • msg.value: value of ether sent with this call

  • msg.gas: amount of gas left in the gas supply of this execution environment. This is amount for a contract because the contract will also take gas to execute. Therefore, if the amount of gas left is not enough for the contract to execute then the contract can "throw an exception" i.e. return an error

  • msg.data: Data payload of the call into the contract. More details on the data inside a transaction is available in this article

A contract's code can refer to the above attributes/data. For e.g. in the above sample contract, the code uses msg.sender to identify the EOA address (or the contract address) that triggered the transaction so that ETH can be transferred from the contract to that address.


Transaction context:

The tx object provides a means of accessing transaction related information:

  • tx.gasprice = gas price in the calling transaction

  • tx.origin = address of the originating EOA for this transaction. If the contract was triggered by another contract then msg.sender will return the address of the calling contract. Therefore, if a contract wants to identify the address of the EOA that triggered the transaction then tx.origin can be used

Block context:

The block object contains information about the current block:

  • block.coinbase = Address of the recipient of the current block’s fees and block reward

  • block.number = current block number

  • block.difficulty = difficulty of current block

Address object:

A address has a number of attributes and methods/functions. Note that this address can be either passed as an input or be the output of another object like the ones discussed above. For e.g. msg.sender will return an address and the following attributes, functions can be used to retrieve information or run an operation on that address:

  • address.balance = Balance of the address

  • address.transfer(amount) = This is a pre-defined function. This function transfers the specified amount to the address

Built in functions:

  • Keccak256, sha256: functions to calculate hashes as hashes are frequently used in Ethereum

  • ecrecover: Recovers the address used to sign a message from the signature

  • selfdestruct(recipient_address): Delete the current contract, sending any remaining ether in the account to the recipient address. As mentioned before, if a contract does not contain this function then the contract cannot be deleted by anyone (not even the creator of the contract)

  • this: the address of the current executing contract. For e.g. address(this).balance returns the balance of the current contract


#2 Contract types

Soldiity’s principal data type is contract. The solidity program in the previous section defines a contract object. As mentioned before, a contract is a collection of data and methods/functions. There are 2 other types of objects that are similar to the contract object type:


Library contract: Library contract is meant to be deployed only once and used by other contracts. Library contract typically contains reusable code. Other smart contracts typically call a library contract using the delegatecall() function. Whenever a contract calls another contract, the values of all attributes of msg change to reflect the new caller’s information. The only exception to this is the delegatecall() function, which runs the code of another contract library within the original msg context. This is clarified in exhibit 3:

Interface: An interface definition is structured like a contract. However, the functions in the interface are only declared and not defined. Therefore, an interface acts like a blueprint or shape that a contract should follow. A contract can "inherit" (to be explained later) an interface contract. By doing so, the inheriting contract can ensure that all the functions in the interface contract are available and defined inside the inheriting contract. For e.g. ERC20 is a popular standard for "tokens" on Ethereum blockchain and the structure for smart contracts that creates an ERC20 token is defined in this interface:



#3 Functions:

To fully understand functions within smart contracts, we need to understand what functions mean within mathematics.

 
A primer on mathematical functions:

A function represents a relationship between an input and an output. Mathematical functions are typically denoted using this symbol: f(). Therefore,


f(inputs) = output


Some examples of mathematical functions are as follows:

Function name

Function notation

Sample input

Sample output

Square function

f(x) = x^2

5

25

Mod function

f(x) = |x|

5

5

Sine function

f(x) = sin(x)

5

-0.95

Square root function

f(x) = sqrt(x)

5

2.236

The square function can take an input like “5” and provide an output of 25. It does so by multiplying the input by itself. This function can also take an input like “-5” and transform it into an output of 25. Therefore, the output for different inputs when applied to a function can be the same. This is known as the “many to one” property of a function i.e. many different inputs can lead to the same output for a function. However, a specific input will always result in a unique output when applied to a function.


We also note that a function can represent a protocol or multiple computation steps and need not be necessarily reduced to a good looking formula like the one shown above. For e.g. the UPPERCASE() function in excel changes the characters of an input string into upper case. This operation can not be easily represented by a mathematical formula as we did for the square function.


uppercase(“abc”) = “ABC”


We will return back to our teardown of the smart contract code.

 

A function inside a smart contract can perform specific operation. The syntax used to declare a function is as follows:


function FunctionName([parameters]) {public|private|internal|external} [pure|constant|view|payable] [modifiers] [returns(return types)]

Function name: This is used to call the function in a transaction (from another EOA) or from another contract. One function in each contract can be defined without a name in which case it becomes the fallback function, which is called when no other function is named. The fallback function cannot have any arguments or return anything


Parameters: We specify the arguments that must be passed to the function, with their names and types. The function operates upon these arguments


Public, private, internal, external specifies the function’s visibility:

  • Public: This is the default value. Such functions can be called by other contracts or EOA transactions or from within the contract

  • Internal: They are only accessible from inside the contract. They cannot be called by another contract and EOA transaction. They can be called by derived contracts (explained later in the article)

  • Private: Private functions are like internal functions but they cannot be called by derived contracts

  • External: External functions are like public functions but they cannot be called from within the contract unless explicitly prefixed with the keyword this

Pure, constant, view, payable keywords affects the function's behavior:

  • Constant or View: Such functions will not modify the state

  • Pure: Neither reads or writes any variables in storage. It can only operate on arguments and return data, without reference to any stored data.

  • Payable: Is one that can accept incoming payments. Functions not declared as payable will reject incoming payments. There are 2 exceptions: Coinbase payments and SELFDESTRUCT inheritances will be paid even if the fallback function is not declared as payable but this makes sense because code execution is not part of those payments anyway

#4 Constructors:

Constructor is a special function type. A constructor is used to initialize the state of the attributes in a contract. When a contract is created, it also runs the constructor function if one exists to initialize the state of the contract. A constructor can be specified in a contract in one of the following 2 ways:

  • function contract_name()

  • Using a special keyword: constructor()

#5 Inheritance:

Inheritance is a mechanism for extending a base contract with additional functionality. The syntax for inheritance is as follows:

Contract Child is Parent {
....
} 

With this, the child contract inherits all the methods, functionalities and variables of the Parent contract.


In the next article, we will do a teardown of an actual smart contract - the ERC20 contract for Basic Attention Token - to see how the above works in practice.


 

Key takeaways:

  • Smart contracts in Ethereum is code that can execute a state change in a trust-less/decentralized manner

  • Smart contracts are transparent, permissionless, deterministic, autonomous and permanent

  • Smart contracts can only be triggered by a transaction (from an EOA) or a message (from another contract)

  • Smart contracts cannot be changed. They can be deleted only if the appropriate "delete" code is included when the smart contract was created

  • Solidity is a popular object oriented programming language for writing smart contracts

  • Ethereum has various objects that expose global pre-defined attributes and functions. For example: msg.address can return the address of the EOA or the contract that triggered the contract in which msg.address was included. The transfer() function can transfer ETH to a specific address

  • Solidity has various constructs (that are typical of object oriented programming languages) like functions, constructors (to initialize the state of attributes in a contract), inheritance (to extend the attributes, functions of one contract to another)