Mastering Smart Contract Development: Writing and Deploying Solidity Contracts using Solidity
August 22, 2023
The Ethereum network is different from the very first Bitcoin network in a way that on the Ethereum network, we can execute smart contracts. But what are smart contracts and how to code them? This post is focused on understanding smart contracts and solidity which is one of the popular programming languages used to write smart contracts.
Let's get started.
What are smart contracts?
Smart contracts are just computer programs. We should not get confused with legal contracts as it has contract word in it. They are a digital transaction protocol that verifies, controls, and self-executes an agreement on the blockchain.
Simply stated, smart contracts can be imaged as a self-executing digital agreement that enables two or more parties to exchange money, property, shares, or anything of value in a transparent, conflict-free way while avoiding the need for a third party.
There are certain sets of properties to use smart contracts efficiently. These are:
- Deterministic: If the same input is given to the smart contract, the output will always be the same.
- Immutable: A contract once put on the network cannot be changed. It can only be deleted using contract accounts (CA). On deleting a contract, only the contract code is deleted, not the contract account containing the contract code. If someone tries to access the deleted code and sends ether as part of the transaction, then those ethers are lost. Hence, it is not advisable to delete smart contracts.
- Limited Execution Context: A contract has access to very limited things on the outside and is limited to EVMs (This will be the next topic of discussion). For example, we can use date in Java to access the date from the clock on your machine, but we do not have this liberty when writing smart contracts.
- Contract Creation: Smart contracts are put on the Ethereum network using contract creation transactions. A new contract account is created and the contract bytecode is put in it. The address of that account becomes the 'id' for that smart contract, and if someone wants to access the smart contract, then they can do so using this id or address.
- EOAs: Smart contracts in Ethereum can only be triggered by Externally Owned Accounts (EOAs). Smart contracts cannot run in the background and they lie dormant until they are triggered by an EOA. Once triggered, they run as defined and then return to the dormant state.
NOTE: If you are not clear with what CA and EOAs accounts are, then I would highly recommend reading the post, Everything you need to know about Ethereum blockchain network.
Next, let's understand how to code such smart contracts on the Ethereum network. Let's explore this further.
Introduction to EVM and Solidity
As an Ethereum node whenever we are running smart contracts on that node (nodes can be imagined to be EC2 instances, windows, or Linux machines) we run them on a very specific runtime environment called Ethereum Virtual Machine or EVM. Every node (popularly called a client) is running on top of EVM runtime.
Further, it is EVM which is a virtual machine that enables a computer or a machine to run smart contracts that are compiled into the EVM bytecode.
Formally speaking, EVM is a runtime environment on which smart contracts are executed. The smart contract codes need to be compiled into the EVM bytecode before they can be executed in the EVM. EVM is present on every node in the blockchain network. The nodes in the Ethereum network are known as Ethereum clients. The two major properties of EVM are that it is:
- Isolated: The isolated nature of EVM enables users to run programs (smart contracts) without being affected by the system or platform on which they are run. It also ensures that the programs running are not affected by any other application on the system.
- Sandboxed: Being a sandbox, EVM is an isolated testing environment that enables users to run programs (smart contracts) without affecting the system or platform on which they run. Hence, they don't affect other applications on the platform.
Now, since smart contracts run on top of EVMs, there is also a need to regularly update the EVMs in terms of the environment and features added to them. How can one achieve this?
Ethereum Improvement Proposal
Such a process is done using EIP stands for Ethereum Improvement Proposal. The Ethereum Development community keeps making such proposals, which are mainly either new features or bug fixes. If the majority of the Ethereum community accepts a proposal, then that EIP becomes a part of the next version of the EVM, which is then updated or installed on every Ethereum client or node. The first version of EVM was called Frontier. And the current version of EVM is called St. Petersburg.
So far so good. Let's understand how the data is stored on the EVM machines.
Storing Data on EVMs
EVMs have three locations under which they can store data. They are:
- Storage: Storage is a persistent storage space present on every Ethereum account. It forms the storage hash which can be thought of as a storage like a hard disc or SSD. The storage retains its value when the computer or machine is not running.
- Memory: Memory is a volatile storage space. Whenever a transaction comes in, some space is allocated to the transaction to execute, and when the transaction ends, its data is deleted from memory, and space is freed. The freed space is then made available for use by the next incoming transaction. We can think of it as a computer's RAM. It disappears when the computer or machine is not running.
- Stack: The EVM is a simple stack-based architecture. All the computations on the EVM are performed on the stack. The stack has a maximum capacity of 1,024 items and the size of each item on the EVM stack is 256 bits. If we run out of the stack, then the contract execution will fail. The compiler generally uses it for intermediate values in computations and other scratch quantities. Once a contract execution is finished, the items containing the contract data are removed from the stack.
Storing data permanently on Ethereum is extremely expensive as it consumes a lot of gas. It is recommended to store only the required data to work properly.
Solidity
Ethereum smart contracts are written in high-level languages such as Solidity, Vyper, Serpent, and LLL. These are then compiled to the EVM bytecode by their respective compilers. And then this EVM bytecode is executed in the Ethereum Virtual Machine (EVM).
Solidity is the most popular language for writing smart contracts, and we will use the same in this post. Solc is the compiler that Solidity uses to compile the Solidity code to the EVM bytecode.
Remix is an online Development Environment that is used to write code (contracts) in Solidity. We can also try to run Solidity on our local machine by installing Solidity using npm or using Solidity's docker image. We can also download and install Truffle, which is a local Development Environment for writing smart contracts.
Refer to the official solidity document for reference purposes.
Remix environment to write contracts
Remix IDE is an online development environment that is used to write, compile, and debug code (contracts) in Solidity. IDE stands for Integrated Development Environment and is an application with a set of tools designed to help programmers execute different tasks related to software development such as writing, compiling, executing, and debugging code.
On Remix IDE following operations can be performed:
- Write, compile, and debug solidity code.
- We can manage plugins in the remix environment.
- Solidity Compiler which is used to compile the code by selecting the compiler.
- Deploy and run Transactions on the test blockchain network.
".sol" is the file extension for a solidity file and "Solc" is the solidity compiler which is used to compile the smart contracts written in the solidity programming language.
Why do we need such IDE? Why can't we directly deploy code on the blockchain network?
The purpose of compiling and running code on an environment like Remix is so that our code is Battle-tested. In other words, we would not want our code to be deployed on a real-world blockchain only to find that the contract code is incorrect.
There could be compilation errors or runtime errors, or some logical mistakes in our contract code. Hence, it becomes important to test our code on a sample blockchain inside Remix or any other similar development environment before deploying the contract on a real blockchain.
Smart Contract Structure
Every .sol file or smart contract file has the following main components:
- One or more Pragma statements: The main purpose of the Pragma statements is to define the Solidity version number. The Solidity compiler, that is, solc, checks for this version number and then convert the Solidity Code to the EVM bytecode accordingly. The version number is important since new functionalities are added continuously and some old functionalities keep getting phased out. A pragma directive is always local to a source file, so we have to add the pragma to all our files.
- One or more import statements: Import statements are used when we want to use the functions defined in some other sol file in our sol file.
- One or more contract definitions: A contract is defined using the contract keyword followed by the series of functions that we write inside the curly brackets.
NOTE: It is very important to specify the version of the contract compiler that we want to use as a pragma statement. The reason is that solidity is in the fast phase of development flow and we may come across scenarios where there is a major upgrade in the compiler and past versions are incompatible with some of the new features released in the newer version.
Below shown code represents the sample solidity contracts:
pragma solidity ^0.5.9; // Pragma statement
import './Canditates.sol'; // Importing another contract
contract Voting { // Contract Definition
}
Smart Contract Objects
There are multiple types of objects that we can use inside our smart contracts. These are:
- State variables
- Function
- Function modifier
- Event
We will go through individual objects that we can use inside the smart contracts. Let's start with state variables.
State Variables
State variables can be defined inside a smart contract using one of the available data types in Solidity. The main data types in Solidity are:
- int: Signed and unsigned integers of various sizes. uint8 to uint256 and int8 to int256.
- bool: Its values are true and false.
- fixed: Signed and unsigned fixed point numbers of various sizes. They can be declared as either fixed or ufixed. Fixed point numbers are not fully supported by Solidity yet. They can be declared but cannot be assigned to or from.
- address: The ethereum accounts are stored in key-value pairs format. The key is a 20-byte string, which represented the account address. It is declared as an address and holds a 20-byte value (the size of an ethereum address).
- Enum: Enums provide a way to create a user-defined type in Solidity.
- Arrays: Arrays can have a compile-time fixed size or they can be dynamic. An array of a fixed size n and element type A is written as A[n], and an array of a dynamic size is written as A[].
- fixed-size byte array: It refers to a fixed-size byte array, which goes from 1 to 32. In other words, the size of the array can range from 1 to 32 bytes. It can be declared as bytes1, bytes2, ...., bytes32.
- dynamic sized byte array: We can declare a dynamic-sized byte array with a maximum size of up to 256 bytes.
- String: String is another type of dynamic-sized array that is used for arbitrary-length string (UTF-8) data.
- Mapping: Mapping types are declared as mapping (_KeyType => _ValueType). Here _KeyType can be almost any type, except a mapping, a dynamically sized array, a contract, an enum, and a struct. _ValueType can actually be any type, including mappings.
- Struct: A struct data type is a group of multiple data types combined together. Solidity provides a way to define new types in the form of structs.
Pro tips when using state variables inside the solidity contract
bytes1 to bytes32 consume less gas as compared to bytes and hence if we know the length or can limit the length to 32 bytes then always use bytesn (where n is the length) and not bytes. byte[] is used to define an array of bytes and is the costliest of the three types(bytesn, bytes, and byte[]) and hence should be avoided.
uint8 can only have 8 bits. That means the largest number which it can store in binary is 11111111 which in decimal is 2^8 - 1 = 255.
Visibility Types
A state variable can have the following visibility types:
- Public: If a state variable is defined as public, then it can be accessed from outside the contract namespace. In other words, it can be accessed from any other contract on the network.
- Private: If a state variable is defined as private, then it can be accessed only inside the smart contract in which it is defined. Even if we import this contract into some other contract, the state variable with private visibility cannot be used from the contract inheriting the parent contract.
- Internal: A variable with internal visibility can only be used from the contract in which it is defined or the contract inheriting the properties of the parent contract. So if a contract 'A' inherits a contract 'B', then it can use the variables in 'B' with the internal visibility type but not with the private visibility type. The variables with public visibility can be used by any contract on the network.
A state variable is defined as <visibility type> <data type> <variable name>;
For example,
public uint votingCount;
internal mapping(address=>bytes32) sampleMapping;
Functions
There are four types of functions in Solidity. These are:
- View: This function type means that the function only reads from the smart contract and will not perform any kind of writing operations. A 'view' function does not consume any amount of gas; hence, if iur function is meant to only read data from the blockchain and not write anything, then we will need to make sure that we define our function type as view; otherwise, the function will end up using some amount of gas.
- Pure: This function type means that the function will neither read nor write anything on the smart contract. They are called helper functions. Math functions are a good example of Pure functions. They will perform some mathematical calculations and return the final computed value. However, these functions neither read from the blockchain nor write on it.
- Payable: Only the functions defined as Payable can take ethers as input. Hence, if we are sending ethers to smart contracts, then we need to make sure that it is only to those functions that are defined as Payable. Sending ethers to non-payable functions will result in failure.
- Fallback: If an incoming transaction call does not match any of the functions defined in a smart contract, then the call will be directed toward the fallback function. In other words, if there is a call for a function that doesn't exist, then the call will fall to the fallback function. There is exactly one Fallback function in every contract. This function cannot have arguments and cannot return anything.
It is not mandatory to define the function type, and, by default, every function is allowed to make changes to the blockchain.
A function in Solidity is defined as follows:
function <function_name>(<arguments to the function>) <visibility_type> <function_type> <modifiers> returns(<return data type>)
The visibility types for a function are the same as the visibility types for the state variables, except there is one more visibility type available for the functions: external. A function with the visibility type as external can only be accessed from outside the contract. A function with external visibility cannot be called from the contract in which it is defined.
Function Modifier
A modifier is a special kind of function that contains certain user-defined conditions. When a modifier is used in a function definition, all the conditions inside the modifier are checked, and if all the conditions are satisfied or are equal to true, only then the function execution moves forward. A modifier acts as a pre-check to the function execution. It is defined with the keyword "modifier" and always ends with '_;' before the closing parenthesis.
modifier onlyAfter(uint _time) {
require(now >= _time,);
_;
}
'_;' is to specify that the modifier is completed and the flow can be passed to the function body where the modifier is used.
Events
Logs are triggered by events. The blockchain is a list of blocks, which are, fundamentally, lists of transactions. Each transaction has an attached receipt, which contains zero or more log entries. The log entries represent the result of the Events having been fired from a smart contract. An Event in Solidity is defined as follows:
event <event_name> (<event_parameters>)
After we have defined an Event, we can call it or trigger it from inside any function we wish. We can call an Event using the emit keyword in the following manner:
emit <event_name> (<event_arguments>)
DApps, or anything connected to the Ethereum JSON-RPC API, can listen to these events and act accordingly. An Event can be indexed so that the Event history is searchable later. We can use the indexed keyword before the parameter on the basis of which we want an Event to be indexed. This is useful in case someone wants to search the history and filter the Events on a particular index. These indexed parameters are stored inside the logs inside a special kind of data structure called 'topics'.
Sample Contract using Solidity
Let's put our learnings into action and write a sample contract to add two numbers.
pragma solidity ^0.5.0;
contract C {
function addition() pure public returns(uint256) {
uint256 a=2;
uint256 b=3;
uint256 c=a+b;
return c;
}
}
With this, we have successfully completed understanding four different types of objects used in solidity i.e., State variables, Function, Function modifier, and Event. Let us next understand global functions that are available to carry out operations in the blockchain network.
Global Functions and Variables
In Solidity, there are special variables and functions which always exist globally. These functions are mainly used to provide information about the blockchain, assist with error handling, employ mathematical and cryptographic functions, and present information about contract addresses and the contracts themselves. The global functions are grouped into four major contexts. They are:
- Address context
- Block context
- Transaction context
- Message context
Address context
Any variable of the data type address can use the global functions defined in the Address context. The in-built functions defined in the Address context are as follows:
- balance: The balance function returns the balance of the Address in Wei. For an address votingcontract, the function call is defined as 'votingcontract.balance;'. This will return the balance of votingcontract in Wei. The return type is uint256.
- transfer(): The transfer function sends the given amount of Wei from the current account to the Address mentioned. If we want to send x amount of Wei from the current account to an address votingcontract, then the function call will be defined as follows: 'votingcontract.transfer(uint256 x);'.
The problem with the transfer function is that if an error occurs during the transaction, then the transaction fails.
- send(): The send function sends the given amount of Wei from the current account to the Address mentioned. If we want to send x amount of Wei from the current account to an address votingcontract, then the function call will be defined as follows: 'votingcontract.send(uint256 x);'.
The return type of the send function is bool, and whenever a transaction encounters an error, the send function returns false. Based on this, appropriate action can be taken. This is what differentiates the send function from the transfer function.
- call(), staticcall() and delegatecall(): These are low-level functions. These functions don't go through the checks by the Solidity compiler, and, hence, it is advised not to use them unless absolutely necessary. The call and staticcall functions work in a similar manner as the transfer or send function.
However, delegatecall works differently. If A invokes B who makes a delegatecall to C, then the msg.sender in the delegatecall will be A and not B. This way we can preserve the original sender of the message. All these low-level functions take bytes as input, and the return type is a combination of bool and bytes data types.
Block context
There are several in-built functions that exist in the global namespace, which are used mainly to provide information about the blocks in the blockchain. The global functions defined in the Block context are as follows:
- block.coinbase: It returns the address of the miner that mined the current block.
- block.difficulty: It returns the difficulty at the time when the current block was mined.
- block.timestamp: It returns the timestamp at which the current block was mined.
- block.gaslimit: It returns the total gas limit of all the transactions mined in the current block.
- block.number: It returns the number of the newest block in the blockchain.
Transaction context
There are functions that exist in the global namespace, which are used mainly to provide information about the transactions in the network. There are two main functions defined as part of the Transaction context. These are as follows:
- tx.gasprice: It returns the gas price of the transaction sent by the sender as part of the transaction.
- tx.origin: It returns the address of the original sender of the transaction.
Message context
There are some global functions that are used to capture the properties of the messages. The main functions in the Message context are:
- msg.value: This function returns the number of Wei that was sent with the message or the transaction.
- msg.sender: This function returns the immediate sender of the message or the transaction. Unlike tx.origin, msg.sender returns the address of the previous account in the flow of the message. If A sends the message to B and then B sends it to C, then if C calls msg.sender on that message, it will receive the address of B as the return value.
- msg.gasleft: This function returns the remaining gas for the transaction. If an account feels that the gas remaining is inadequate or insufficient for a transaction to complete, then it will fail the transaction.
Other functions
Apart from the functions belonging to the four contexts that we saw, there are two important global functions in Solidity. These are as follows:
- now(): This function returns the timestamp when the last block in the blockchain was created. Since the block creation rate in Ethereum is approximately 15 seconds, the timestamp returned will be approximately 15-30 seconds prior to the current time. The timestamp returned by now() is the number of milliseconds since the Unix Epoch time.
The timestamp returned by now() is the number of milliseconds since January 1, 1970. If a miner's clock is out of sync with other miners’ clocks by more than about 12 seconds then that miner will have trouble connecting to peers. This is done so that miners are not able to manipulate the creation time of a block. In other words, this is done to prevent the manipulation of time by miners.
The time returned from the now() function depends entirely on the clock in the machine which mined that particular block.
- selfdestruct(): As the name suggests, this function is used by a contract to destroy or kill itself. We need to pass an address as an argument to this function, and all the balance ethers that are present on the contract account will be transferred to the account with the above address.
It has been quite a lot that we have covered so far. If you are overwhelmed I would recommend you take a pause and go through the content one more time. It will make sure that the basics of solidity are clear. But if you are excited, let's move forward and understand the lifecycle of the smart contract.
Lifecycle of smart contracts
Let's take a look at what happens at the time of installation of a smart contract over a network and what happens before a smart contract is destroyed. There are two major elements that define the birth and death of a smart contract in Solidity. They are:
Constructor
A constructor is a function declared with the constructor keyword. It is called when a contract is installed on the network. It is executed only once and is never executed again.
A constructor is optional and only one constructor is allowed. After the constructor has been executed, the final code of the contract is deployed to the blockchain. The code deployed does not include the constructor code or the internal functions called from the constructor. Other than this, the code deployed includes everything defined inside the contract.
A constructor is defined as follows:
constructor(<parameters>) <visibility_type>
{
<code that we want to execute at the time of contract deployment/installation>
}
Since the constructor is the first-ever function to be called in a smart contract, it is called part of the contract creation call, which can be made only by an externally owned account. Hence, it is advisable to keep the visibility type of the contract public. In fact, Solidity only allows the visibility type of a constructor to be public or internal. A constructor cannot be private or external.
Selfdestruct
The selfdestruct function kills or deletes the entire smart contract from the blockchain when the function is called. However, this does not delete the contract account, and the contract account on which this contract code was deployed becomes a blank account.
A selfdestruct function is as follows:
selfdestruct(<address of the account to which the balance ethers will get transferred to>) ;
We need to pass an address of an account as a parameter to the selfdestruct function. This address is the address of the account to which all the balance ethers in the current contract account will be transferred.
NOTE: We can kill the smart contract code in selfdestruct but the contract account (CA) on which the smart contract is installed is never deleted. Therefore the CA account will end up being a blank account.
To summarize, constructor and selfdestruct are two important functions that are called during the installation and destruction of the smart contract respectively on the blockchain network.
Inheritance in Solidity
In object-oriented programming, inheritance is a feature that represents the 'is a' relationship between different classes or contracts. Inheritance allows a class or a contract to possess the same behavior (functions and variables) as another class or contract and allows modifying that behavior as per the need.
By means of inheritance, we can use the variables, functions, modifiers, and events of base contracts in the derived contract. In Solidity, a contract that is inherited is called the parent or the base contract, and a contract that inherits is called the child or the derived contract.
All public and internal scoped functions and state variables defined in the parent contract are available for use in the child contracts. Inheritance is a very important concept because it brings the code-reusability feature to the table.
To inherit a contract in Solidity, we need to perform the following steps in our child contract:
- Use the import statement to import the parent contract. The syntax for the same is as follows:
import '<path to the parent contract file>';
- After importing the file, we need to make the current contract an inherited contract. We can do this using the 'is' keyword. The 'is' keyword is used to inherit the base contract in the derived contract. The syntax for the same is as follows:
contract <contract_name> is <contract_name of the parent class>{<contract code>}
Therefore, inheritance allows to use the accessible function from another smart contract as well in the parent contract.
Error Handling
There are three methods that constitute the error-handling process in Solidity. These are as follows:
require()
require() is used to validate certain conditions before further execution of a function. It takes two parameters as input. The first parameter is the condition that we want to validate and the second parameter is the message that will be passed back to the caller if the condition fails.
If the condition is satisfied, then the execution of the function continues and the execution jumps to the next statement. However, if the condition fails, then the function execution is terminated and the message (the second parameter) is displayed in the logs. The second parameter, however, is optional. require() will work even if we pass only one parameter, that is, the condition to be checked.
The require() statement is defined as follows:
require(<condition to be validated> , <message to be displayed if the condition fails>);
In require(), if the condition fails then the function call is reverted back to the initial state.
assert()
The assert function, like require, is a convenience function that checks for conditions. If a condition fails, then the function execution is terminated with an error message. assert() takes only one parameter as input. We pass a condition to assert(), and if the condition is true, then the function execution continues and the execution jumps to the next statement in the function.
The assert() statement is defined as follows:
assert(<condition to be checked/validated>);
In assert(), if the condition fails then the function call is reverted back to the initial state.
revert()
The revert function can be used to flag an error and revert the current call. We can provide a message as well containing details about the error, and the message will be passed back to the caller.
if(condition) {
revert("Condition failed. Reverting the transaction.");
}
revert() causes the EVM to revert all the changes made to the state, and things return to the initial state or the state before the function call was made. The reason for reverting is that there is no safe way to continue execution because something unexpected happened.
This is important as it helps in saving gas. Since the function execution stops after the revert() statement, the remaining gas is also returned back to the user. If we don't use the revert() statement and some error occurs, then the entire gas is lost. Using revert() does not return the consumed gas, however, the gas that is consumed is consumed, so it cannot be returned.
Difference between require(), assert(), and revert()
As operations are performed inside a function, a substate is maintained. Now if the condition in require() or assert() fails, or if the revert() method is called, then the substate vanishes and things return to the initial state. However, if the function executes till completion, then the final substate is considered the finalized state or the new state. This ensures that the atomicity of the Ethereum states is maintained. If a function fails, things return to the initial state, or if a function executes successfully, then things move to a new state.
Generally, require is used to check the conditions on the input parameters that act as input to the function, whereas assert is generally used to check for any internal errors. In other words, require is used at the start of the function and acts as a pre-check to the function, whereas the assert statement is used in between the operation statements inside a function and is used to check whether a certain thing is leading to an error or not.
The other major difference between the two is that the unused or remaining gas is not returned back in case an assert() exception occurs. However, when using require(), if the condition fails, then the unused or the remaining gas is refunded back to the user.
Now if the unused gas is not refunded back in case of assert failure, why should we use assert? The answer is, assert is used to indicate that this condition [which is passed inside assert()] should never fail; if, however, this condition fails, then that means there is something very wrong with the code.
Deploying and Testing of smart contract on Remix
The purpose of compiling and running our code on an environment like Remix is so that our code is Battle-tested. In other words, we would not want our code to be deployed on a real-world blockchain only to find that the contract code is incorrect. There could be compilation errors or runtime errors, or some logical mistakes in our contract code. Hence, it is highly advisable to test the code on a sample blockchain inside Remix or any other similar development environment before deploying the contract on a real blockchain.
On the Remix IDE, the following are the parameters that we have to configure before we deploy the smart contract on the sample blockchain.
- Environment: This is a sample mock Ethereum environment where we can deploy and test our smart contract before deploying on a real Ethereum network.
- Accounts: These are sample accounts provided with preloaded ethers for us to test the smart contract.
- Gas Limit: Evert transaction that will be triggered from the smart contract in the testing environment, here we can specify the gas limit for each of such transactions. If we remember, the sender can set the gas limit of how much he wants to maximum spend as part of the transaction. This gives such feasibility.
- Value
After specifying the value, next, we can go ahead and deploy the smart contract. If the constructor defined accepts any input parameters, we need to pass such input parameters before we go ahead and deploy the smart contract.
How to access already deployed smart contracts in the Ethereum network?
As we explored earlier, the contract code is stored as the EVM bytecode on a contract account in the blockchain. When we inherit a contract, the Solidity compiler copies the bytecode of the base or the parent contract into the bytecode of the child or the derived contract. In other words, the bytecode present on a contract account contains the bytecode for the main contract as well as for all the other contracts that are imported into that contract.
Now, what if there is a situation when we need to access a smart contract that is already deployed on the network?
Ethereum highly discourages accessing a smart contract that we have not deployed. Hence, the main use case of accessing smart contracts is when we need to access a smart contract that we have created. We can access an already deployed smart contract by following two ways:
- Either create a new instance of the deployed smart contract.
- Using an existing instance of the deployed smart contract that we want to access.
Both these approaches work only when we have the Solidity file for the contract that we are trying to access. And if we have created a contract earlier, then we are expected to have the Solidity file for that contract. Let's understand both of the ways to access already deployed smart contracts.
Access a smart contract by creating a new instance of that contract
When we create a new instance for a smart contract that is already deployed on the blockchain, a new contract account is created, which contains the same contract bytecode as the original contract account. Now we access the functions of the contract using the address of the new contract account.
We can define a new instance as follows:
import 'deployed_contract_name.sol';
contract child is <deployed_contract_name> {
<deployed_contract_name> <variable_name>;
<variable_name> = new <deployed_contract_name>(<parameters to the constructor of the deployed smart contract>);
}
Here, the <variable_name> stores the address of the newly created instance of the deployed smart contract. The new contract account will contain the bytecode of the <deployed_contract_name>. Now, we can call any function of the contract using <variable_name>.<function name and function parameters>.
Access an already deployed smart contract using an existing instance of the contract
We can access another contract using an already existing contract instance but we still need a solidity file of the contract. To do this, we need to define a variable of that contract, and then in that variable, we need to store the address of the contract account that contains the contract bytecode that is to be accessed.
We can use an existing instance as follows:
import 'deployed_contract_name.sol';
contract child {
<deployed_contract_name> <variable_name>;
<variable_name> = <deployed_contract_name>(address of an already existing contract account that contains the contract code to be accessed);
}
Here, the <variable_name> stores the address to the existing contract account. Now we can call any function of the contract using <variable_name>.<function name and function parameters>. This approach is different from the first approach where we are not creating a new instance of the already deployed smart contract. Also, we will notice that we are not using new keywords when creating an instance of the deployed smart contract.
Now suppose we want to access a contract whose Solidity file we don't have. This is generally the case when we try to access a smart contract that is deployed by someone else. We can achieve this by using low-level functions, such as call() or delegatecall(), to access contracts whose Solidity files we don't have.
For accessing a smart contract, we need to know the address of the contract account that stores the bytecode for the contract that we are trying to access. Using this address and the low-level functions, we can access this contract.
To make a function call on an already deployed contract, we can use the call or delegatecall() functions as follows:
<address>.call(<function name and parameters>);
<address>.delegatecall(<function name and parameters>);
Using low-level functions like call() and delegatecall(), we can access the elements of any contract irrespective of who created the contract. We only need the address of the contract account which stores the contract bytecode. But, it is highly advisable to not access a contract that we have not created.
Bonus Topic: How decentralization is achieved for an application or DApps are designed?
There are two kinds of applications: Distributed applications and decentralized applications. Both kinds of applications can be divided into four major layers, that is, frontend, backend, communication, and database.
If one or all of the layers are decentralized they are called DApps or Distributed Apps. Every decentralized application is distributed in nature. Generally, for DApps we decentralized the backed or logic layer of the application using Ethereum.
- Ethereum decentralizes the backend or the logic layer of the distributed application. It does so by means of smart contracts.
- Swarn decentralizes the front-end layer of the application.
- Whisper decentralizes the communications layer of the application.
If every layer of a distributed application is decentralized, then that application becomes a completely decentralized application. Therefore, centralized apps are not necessarily distributed in nature, but Decentralised Applications (DApps) are always distributed.
Let us look at the architecture of the DApps which is decentralized only by the backed or logic layer of the application.
Users interact with the application using the user interface, which is the front end of the application. Web3.js is one of the tools that connect the front end to the back end of the distributed application i.e., smart contracts installed over the blockchain nodes.
Summary
To summarize, we have covered what smart contracts are and how to code, debug and test our own written smart contract using solidity as a programming language on the Remix IDE. We have thereafter gone and explored four different objects i.e., State variables, Function, Function modifier, and Event that we can use to write the smart contract.
We have thereafter extended the knowledge and explored global functions and variables, Lifecycle of smart contracts, inheritance in solidity, and error handling. And we finally concluded the post by exploring different ways to deploy and access already deployed smart contracts.