Skip to main content

16 posts tagged with "solidity"

View All Tags
Go back

· 11 min read
Mayowa Julius Ogungbola

Introduction

Non-fungible tokens (NFTs) have gained significant popularity in recent years due to their ability to represent unique digital assets such as artworks, collectibles, and even virtual real estate. One specific type of NFT, called ERC1155, allows for the creation and management of both fungible tokens within a single smart contract.

Minting your own NFTs on Celo is a relatively straightforward process. this tutorial will provide the necessary steps to successfully mint your ERC115 NFT on the Celo Blockchain network, with tips and best practices for successful minting, using the Remix IDE.

Additionally, this tutorial will provide a summary of the advantages of using the Celo blockchain to mint ERC1155 NFTs. And helps you demystify the process and provides the tools and resources necessary for successful NFT minting.

Prerequisites

Remix: You should be familiar with working on the Remix IDE. This tutorial will make use of the Remix IDE to create, deploy and mint your ERC token on the Celo Blockchain.

OpenZeppelin: For creating your ERC1155 Token standard contract, you’ll make use of an already created and secured contract on the openzeppelin library available remotely on the internet.

This tutorial requires you to have a solid foundation knowledge of basic web3 concepts like NFTs, openzeppelin, smart contracts, solidity code, etc.

Note: Although this tutorial will cover creating and deployment of your ERC contract, it will not include an in-depth explanation of how to create and deploy your ERC115 contract.

Requirements​

To follow this tutorial you should have the following:

  • Celo Wallet Extension: This tutorial will require you to have an account on an installed celo wallet extension, or if you’re using another wallet like metamask, you should have the celo alfajores network added. Here is a link to guide you on how to add the alfajores testnet to your custom wallet.

  • Faucets: You should also have your wallet funded with Celo test funds. Here is a link to request celo faucets.

  • Node & node package management npm or yarn: This tutorial will require you to use a preinstalled node package manager, yarn. You should also know about working with any package manager: npm or yarn.

  • IPFS: Although this tutorial will not cover uploading your NFT images on IPFS, you should also be familiar with the concept of uploading on IPFS, and how to upload files on IPFS.

Note: IPFS (InterPlanetary File System) is a distributed file system that allows users to store and access content from anywhere in the world. It is built on the same principles as the Ethereum blockchain, making it the ideal choice for hosting and delivering content on the blockchain.

The ERC115 Token Standard

Before getting started with writing code and minting your ERC115 token, here is a quick reminder on what the ERC1155 token is and how it is usually minted.

The ERC115 Token Standard is one of the most popular Ethereum-based token standards, that is used for creating, minting, storing, and transferring Non-Fungible Tokens (NFTs).

It is an extension of the ERC20 Token Standard and allows users to create unique tokens with different attributes, such as name, symbol, and metadata. And a powerful tool for developers to easily create and manage digital assets on a blockchain network.

Tokens created with the ERC1155 Token Standard are also interoperable with other ERC20 tokens, for the creation of multiple asset types, such as collectibles, game items, digital artwork, and more. To mint a new token, developers need to call the ERC1155 contract and pass in the token's contract address, an array of token IDs, and an array of tokenURI strings. The tokenURI string is used to identify each token and can contain information such as a token’s title, description, or image.

The following code example shows the functions to mint a new ERC1155 token from a smart contract on a blockchain network. From openzeppelin’s standard ERC1155 contract:

function _mint(address to, uint256 id, uint256 amount, bytes memory data) internal virtual {
require(to != address(0), "ERC1155: mint to the zero address");

address operator = _msgSender();
uint256[] memory ids = _asSingletonArray(id);
uint256[] memory amounts = _asSingletonArray(amount);

_beforeTokenTransfer(operator, address(0), to, ids, amounts, data);

_balances[id][to] += amount;
emit TransferSingle(operator, address(0), to, id, amount);

_afterTokenTransfer(operator, address(0), to, ids, amounts, data);

_doSafeTransferAcceptanceCheck(operator, address(0), to, id, amount, data);
}

Uploading the NFT image on IPFS

To get started with minting your NFT on the Celo blockchain. First, you need to have your image uploaded on IPFS, although this tutorial will not cover how to upload your pictures, here is a video reference to learn how to upload your NFT images to IPFS using NFT UP, Since you will need an already uploaded NFT image for this tutorial, you can use the link to an NFT folder with the images PHENZIC000 and PHENZIC001, uploaded on IPFS.

Minting your ERC1155 Token Standard

Remix is an open-source web IDE that simplifies the process of creating and deploying smart contracts to a blockchain network. It offers a simple graphical user interface that enables you to write and deploy Ethereum contracts quickly and easily. To get started with minting and interacting with your tokens, you’ll need to create a basic template for your smart contract on Remix, hence the following steps:

  1. First, head over to the Remix IDE using this link,
  2. You will need to download the Celo plugin on Remix, for interacting, compiling, and deploying on the Celo blockchain. Following the images and steps below.
  • a. Click on the plug-like icon on the left bottom part of the screen.

  • b. Enter Celo into the search bar to search for the Celo plugin.

  • c. Click Activate to add the Celo plugin to the left plain, you will notice the Celo icon has been added to the plugins on the left, click on the Celo Icon.

plugin_manager

  1. Next, create a new file under the contracts directory, and name the file MyToken, where you will have your smart contract written.

files

  1. Copy and paste the following code below into your MyToken contract:
  • a. The code below simply initializes the contract by importing the standard ERC1155 token contract from openzeppelin into your contract, including all the functionalities of a standard ERC1155 token:
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts/utils/Strings.sol";

Note: OpenZeppelin is an open-source framework for developing secure, reliable, and upgradable smart contracts on the blockchain. It provides a large library of reusable, secure, and well-tested components that can be used to quickly develop and deploy applications on the Ethereum blockchain.

Copy and paste the code above inside your MyToken.sol contract file.

  • b. Next, the function below creates a new contract as a sub-contract of the standard openzeppelin ERC1155 contract and also initializes two constants for the NFTs you will be creating.
contract MyToken is ERC1155 {
uint256 public constant PHENZIC000 = 0;
uint256 public constant PHENZIC001 = 1;
}

Copy and add the code above to your MyToken contract file.

  • c. The Next function is the constructor function that takes in the link to your NFT images uploaded on IPFS and calls the _mints function to mint your NFTs.
    constructor() ERC1155("https://bafybeiaqqz4unoubpu2oz2rsgowh3irdqnpcqjoyspzwrepnrwql7rgvy4.ipfs.nftstorage.link/"){
_mint(msg.sender, PHENZIC000, 200, "");
_mint(msg.sender, PHENZIC001, 100, "");
}

Copy and add the code above to your token contract.

  • d. Finally the last function uri take in unit256 digit 0 or 1. And returns the direct link to any of the NFT's locations on IPFS, either the first no or the second.
    function uri(uint256 _tokenId) override public pure returns (string memory) {
return string(abi.encodePacked("https://bafybeiaqqz4unoubpu2oz2rsgowh3irdqnpcqjoyspzwrepnrwql7rgvy4.ipfs.nftstorage.link/PHENZIC00",Strings.toString(_tokenId),".jpeg")
);
}

Copy and add the code below inside your token contract.

  • e. Finally, your complete contract should look exactly like the one code below, you can copy and replace your entire code with this for uniformity's sake.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts/utils/Strings.sol";

contract MyToken is ERC1155 {
uint256 public constant PHENZIC000 = 0;
uint256 public constant PHENZIC001 = 1;

constructor() public ERC1155("https://bafybeiaqqz4unoubpu2oz2rsgowh3irdqnpcqjoyspzwrepnrwql7rgvy4.ipfs.nftstorage.link/"){
_mint(msg.sender, PHENZIC000, 200, "");
_mint(msg.sender, PHENZIC001, 100, "");
}

function uri(uint256 _tokenId) override public pure returns (string memory) {
return string(abi.encodePacked("https://bafybeiaqqz4unoubpu2oz2rsgowh3irdqnpcqjoyspzwrepnrwql7rgvy4.ipfs.nftstorage.link/PHENZIC00",Strings.toString(_tokenId),".jpeg")
);
}
}

  1. Next click on the Celo plug-in icon you activated earlier, to compile and deploy your contract on celo.

  2. Now, click on the connect button attached to the first input bar on the side plain, to connect to your wallet, you will notice it automatically returns the amount of native token on your alfajores wallet and also tells you the network you are on currently.

Note: Also ensure you are current on your celo alfajores testnet account on any wallet of your choice, and the account should already be funded with Celo testnet native token.

  1. Next, Click on the compile button, and Remix should compile the contract without any errors.

compile_token

  1. Now click on the deploy button below, and your wallet will pop up on the right side of your screen for you to sign the transaction.

deploy_myToken

  1. Click on confirm button, and you should have your contract already deployed to the celo alfajores testnet with the address of your contract on the display tab attached to the deploy button.

confirm_transaction

Note: The NFTs minted will be signed to the address of the person that deployed the contract and called the function.

  1. Now, that you have deployed your token contract with the minting function being called inside the contract’s constructor function. Your NFTs have automatically been minted to your address.

Interacting with the Deployed ERC1155 Token

Now that you have your newly minted ERC1155 token deployed and minted on Celo alfajores signed with your address. You can run some checks to interact with your token on the blockchain.

  • To check the number (balance) of NFT currently available on your wallet address:

a. Click on the function call balanceof at the bottom left side of your screen.

b. You will notice two drop-down input tabs to add your connected wallet address and the token Id of the NFT you want to view its balance, Copy your connected wallet address and past it in the first field, and input 0 in the second tab to view the balance of the NFT.

c. Click Call to view the amount of the PHENZIC000 token available in your address.

remix_balance_Of

  • Assuming you have a Gaming application where you welcome each new player by gifting them with the PHENZIC000 token and gifting the old-timer player with certain levels attained in the game with the PHENZIC001 token. You can have this done automatically on your Dapp but in this case, you can equally send these tokens by calling the safeTransferFrom function.

a. First, click on the safeTransferFrom function call like in the image below to add the required parameters to call the functions.

b. Copy and add your connected wallet address as the first input for the variable from.

c. Next, add the wallet address you want to send the NFT to, any remote alfajores address will work fine.

d. Enter the id of the token you want to send 0 or 1, and the amount you want to send for this example you can use 0 for id, 50 for the amount, and use 0x00 for the data input.

e. Click transact, and to check the balance of the remaining token.

f. Head back to the balanceof add your connected wallet address and 0 or the id of the token you sent from, and click call. You will notice the reduction in the token amount.

functions

  • The uri function call overrides simple taken in a token_id, and returns the link to either the first minted NFT or the second minter NFT on IPFS, depending on the token_id you input.

Conclusion

Minting your ERC1155 NFT on the Celo blockchain is a relatively straightforward process, but it does require some technical knowledge and setup. By following the steps outlined in this tutorial, you can successfully mint your ERC115 NFT and begin it in the Celo ecosystem.

You would have used the Celo plugin on the Remix IDE to interact with the Celo blockchain and, understand other concepts like how the IPSF works and how to mint your token contract using Remix.

About the Author

Mayowa Julius Ogungbola

is a Software Engineer and Technical writer always open to working on new ideas. I enjoy working on GitHub and you can also connect with me on LinkedIn.

Next Steps

Here are some other NFT-related tutorial articles you might be interested in:

References​

Here are links to some video tutorials you might be interested in following along with while reading this tutorial:

Go back

· 11 min read
Mayowa Julius Ogungbola

header

Introduction

The Celo blockchain is a proficient, fast, and lightweight platform that supports the building of innovative, complex, and client-designed mobile applications. In simple terms, it is a network that allows the development of decentralized and inventive mobile and web applications.

One of the core features of how the Celo blockchain work falls on the concept of multiple revolutionary solutions, one of which is called Proof of Stake (PoS). On completing this article, you’ll have a solid idea of the Concept of Celo’s protocols, what PoS is and how it makes Celo an efficient platform for creating indigenous, decentralized solutions.

You’ll also have all the basic information you’ll need to get started with building on the Celo blockchain.

Prerequisites

Throughout this article, you are not expected to have any prior in-depth knowledge of any technology or intricate detail about the web3 space. If you’re reading this tutorial, it means you want to know more about what a PoS is and how the Celo Blockchain integrates this verification system.

What is Consensus Mechanism

A Consensus Mechanism is a method of authentication adopted by blockchain platforms to ensure transactions are in sync and agree on which transaction is valid before adding the transaction to the blockchain.

Amongst others, one of the proven efficient and effective means of reaching consensus on the blockchain is using the Proof of Stack PoS consensus mechanism. Which is why the Celo blockchain uses it.

When a transaction is created on the Celo blockchain before it is added to the blockchain ledger it first needs to be validated by the chain’s miners, thus the need for consensus.

What is Proof of Stack (PoS)

Proof of Stack is a type of consensus mechanism that adopts the idea of staking coins to earn its node runners the right to validate a transaction before adding it to the blockchain ledger.

When a transaction occurs on a blockchain platform like Celo, there is a need to first authenticate and validate the transaction before adding the transaction to the blockchain permanently. These tasks are usually carried out by the blockchain’s miners and node runners on the Celo blockchain.

Note: Node runners or validators are individuals or companies running full blockchain network nodes. They provide the backbone of the blockchain network by providing the infrastructure that allows the network to process transactions and maintain a distributed ledger. Node runners are rewarded for their services with tokens or coins from the network.

Looking back to the technology system before the web3 revolutionary breakthrough, verifying transactions would require a centralized or automated entity that was prone to either time consumption for financial and data forgery for non-monetary transactions and a lot more.

The need for a system like the PoS for the validation of transactions, therefore came as an innovative technological breakthrough.

In a PoS system, the chances of forgery or manipulation of transactions and data would be easily spotted and penalized. The PoS means of consensus is one of the important features that make the Celo Blockchain a secure and rewarding platform for developers to build on and validators to manage and get rewarded for.

The PoS algorithm also allows its validators and node runners to carry out validation while maintaining a low computational cost and manual effort.

Other types of Consensus Mechanism

Just like the PoS is used by the Celo blockchain as a means of reaching Consensus in approving all transactions, there are also other consensus mechanisms adopted by other blockchains some of which are;

  1. Proof of Work (PoW): This type of consensus algorithm requires its miners to consume a massive amount of computational power to solve the complex cryptographic puzzle that defines each transaction. Although this algorithm is effective and adopted by popular blockchain platforms, it is not quite efficient.

  2. Delegated Proof of Stake (DPoS): This type of consensus algorithm is similar to the PoS algorithm. But unlike the PoS algorithm, a group of miners or node runners is tasked with achieving a distributed consensus. It serves more like a voting system where miners decide who should be responsible for validating a transaction.

  3. Proof Of Importance (PoI): This algorithm is also very similar to the Proof of Stake algorithm, In the mechanism rewards are given to effective network users. The algorithm allows a hierarchy of importance based on individuals who have made more contributions to the system and gives them the right to validate transactions on the platform.

  4. Proof of Activity (PoA): This algorithm is a hybrid of the Proof of Work (PoW) and the Proof of Stake (PoS) algorithms, that allow validators to stake coins as collateral, and also validate blocks using computational decryption and other resources.

  5. Proof of Captivity (PoC): Thissystem of consensus neither requires a miner si solve cryptographic puzzles like the PoW or stake coins like the PoS, but rather to prove that they have a certain amount of hard drive space to contribute to storing plots of cryptographic hashes for the blockchain.

  6. Proof of Authority (PoA): This algorithm simply pre-selects and authorizes validator nodes to validate a newly added block.

  7. Proof of Elapsed Time (PoET): This system of consensus was designed to simply select a leader from a pool of miners and validators and assigns the task of validating the next node.

These are a few other important consensus algorithms available and adopted by other blockchain platforms.

Other Core Feature Of the Celo Blockchain

  • Scalability: The Celo blockchain was built to handle a large number of users and transactions without slowing down or becoming unresponsive. Celo is a layer-1 blockchain solution that helps to scale up blockchain technology to handle a high throughput of transactions. It does this by allowing transactions to be validated off-chain and then quickly grouping them into batches for faster processing on-chain. This helps reduce overall network congestion and improves scalability.

  • Security: Transactions on the Celo Blockchain go through a set of measures taken to protect the data and transactions stored on the blockchain from tampering or unauthorized access. This includes encryption, secure protocols, and validation of transactions. Additionally, Celo's consensus protocol is designed to ensure the safety and integrity of the blockchain by maintaining a decentralized network of validators who are incentivized to keep the blockchain secure.

  • Speed: The Celo blockchain is designed to offer a high level of speed, and enables users to quickly and securely transfer value across the network in a matter of seconds. It also utilizes a sharding technology called Celo Fast Finality (CFF) that allows the network to split its transactions into multiple shards to process thousands of transactions per second. It utilizes a mechanism called instant finality that also allows the network to commit transactions.

Building on Celo

As a developer looking to build fast, secure, scalable, and innovative ideas, building on Celo is an exciting opportunity for you to create applications and services that leverage the Celo platform. Celo provides a secure, open-source platform for developers to create distributed applications and services that connect people and organizations in meaningful ways. With Celo, developers can create applications that bring new possibilities to the global economy, from unlocking financial inclusion to providing access to new markets.

Wallets

The Celo wallet enables its users to quickly and securely send and receive payments, store digital assets, and access various financial services. It allows users to view their account balances and track their transactions.

The wallet also provides developers with the tools they require to interact with the Celo Blockchain, like signing transactions, deploying and testing contracts, calling and testing functions, etc. Additionally, the Celo wallet offers enhanced security features to ensure the safety of users’ digital assets. With its suite of features, the Celo wallet is a powerful tool for building on the Celo blockchain. Indirectly you can also interact with the celo blockchain by adding the celo network to other wallets like metamask, etc. More wallet-related information and why you need one can be found here.

Smart Contract

The Celo blockchain also allows technological transactions like compiling, testing, debugging, deploying, and calling contracts on the network, which gives you the ability to create, a decentralized application and interact with a decentralized codebase on the blockchain.

Here, you will find more tutorials on creating, verifying, and deploying smart contracts, writing contract tests, and making contract call on the Celo blockchain using, Hardhat, Truffle, foundry, Remix, etc.

Connecting to Celo

When connecting to the Celo alfajores or maiNet for deploying or interacting with the network, you’re advised to use the Celo configuration file below in place of the code in your .config file.

require("@nomiclabs/hardhat-waffle");
require("dotenv").config({ path: ".env" });
require("hardhat-deploy");

// You need to export an object to set up your config
// Go to https://hardhat.org/config/ to learn more

// Prints the Celo accounts associated with the mnemonic in .env
task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
const accounts = await hre.ethers.getSigners();

for (const account of accounts) {
console.log(account.address);
}
});

/**
* @type import('hardhat/config').HardhatUserConfig
*/
module.exports = {
defaultNetwork: "alfajores",
networks: {
localhost: {
url: "http://127.0.0.1:7545",
},
alfajores: {
gasPrice: 1500000000,
gas: 4100000,
url: "https://alfajores-forno.celo-testnet.org",
accounts: {
mnemonic: process.env.MNEMONIC,
path: "m/44'/52752'/0'/0",
},
//chainId: 44787
},
celo: {
url: "https://forno.celo.org",
accounts: {
mnemonic: process.env.MNEMONIC,
path: "m/44'/52752'/0'/0",
},
chainId: 42220,
},
},
solidity: "0.8.10",
};

Here is a link to the code sample above, you will also require your funded wallet’s Mnemonic phrase in an encrypted file. To know more about interacting with the Celo blockchain here are some tutorials you can read on.

Creating Dapps on the Celo blockchain

The Celo blockchain provides the infrastructure for decentralized applications (DApps) to act as a bridge between users and their data privacy. The increasing number of dApps that utilize the Celo blockchain validates its usefulness in the blockchain ecosystem.

Creating the Celo Blockchain is a revolutionary way to use blockchain technology to build secure and reliable mobile and web applications. With the Celo platform, developers can easily create decentralized applications (DApps) and smart contracts that enable users to transfer value, store information, and more. This platform is designed to be user-friendly and secure, making it an attractive option for developers looking to build the next generation of applications.

Redeploying to Celo

Redeploying your Decentralized Application (Dapp) to Celo is a necessary process that can enable you to take advantage of the Celo network’s advantages, including scalability and security. The Celo platform enables developers to create and deploy their Dapps quickly and easily, and redeploying is an important part of this process.

Redeploying your Dapp to Celo requires a Celo account, which you can create through their website or the Celo mobile app. Once you have an account, you must create a Dapp and deploy it to the Celo blockchain. This process involves setting up a smart contract, enabling the Celo network to run your Dapp. You then need to write the code for your Dapp and deploy it onto the Celo blockchain. Once your Dapp is deployed, it will be available to anyone on the Celo platform. Here you can find more information on Redeploying your Dapp to the Celo network.

Redeploying on the Celo network provides the following benefits to your decentralized application.

  1. Scalability: The Celo blockchain can scale to millions of users, with each user able to transact in a matter of seconds. This makes it ideal for large-scale applications that handle large volumes of transactions.

  2. Low Cost: Celo has committed to providing users with low-cost transactions, allowing developers to lower the cost of running their Dapps and making them more attractive to users.

  3. Security: Celo utilizes advanced cryptography to provide a secure and reliable platform for developers and users.

  4. Ease of Use: Celo has a user-friendly interface that makes it easy for developers to deploy their Dapps and for users to use them.

  5. Open Source: Celo is an open-source platform that allows developers to access and customize the code to their needs and requirements.

  6. Community Support: Celo has a vibrant and supportive community of developers and users who are always willing to help out and provide assistance.

Conclusion

Now that you have completed this tutorial, you understand the concept of one of the core concepts that make up the Celo blockchain. And you now have everything you need to start building and interacting with the celo blockchain.

Next Steps

Now that you’ve successfully grasped the lesson in this tutorial, you can also read on the following topics to help you get started on building real-world solutions and other development on Celo.

You can also consider contributing to the Celo network as a developer or as a technical writer (Celo Sage).

About the Author​

Mayowa Julius Ogungbola

A Software Engineer and technical writer who is always open to working on new ideas. I enjoy working on GitHub, and you can also find out what I tweet about and connect with me on LinkedIn

Go back

· 14 min read
Mayowa Julius Ogungbola

header

Introduction

When creating decentralized applications that leverage smart contracts, it is important to ensure that there are little or no vulnerabilities to prevent an attacker from compromising your application.

Unit testing helps you ensure that all functionalities in your contract are working as expected, and development environments like Truffle give you the same tools to help you write proficient tests for your contracts before final deployment.

In this tutorial, you’ll create an exemplary contract and learn how to write and run unit tests for your contract using the truffle development environment.

Prerequisites

Throughout this tutorial you’ll need to have worked with or have a basic knowledge of the following:

  • Truffle Suite: Truffle suite is a Development Environment that acts as a pipeline for interacting with the EVM and also provides essential features and valuable libraries for testing Ethereum smart contracts and makes it easy to interact with the blockchain.
  • Solidity: Solidity is a high-level programming language used for creating smart contracts.
  • Javascript: This tutorial will make use of Javascript, therefore you should be familiar with basic Javascript coding and algorithms.

Requirements

This tutorial also aspects that you have the following already installed or available:

  • Node & node package management npm or yarn: This tutorial will require you to use a preinstalled node package manager. You should also know about working with any package manager: npm or yarn.

Installing and setting up Truffle suite

To install the truffle suite using your terminal. Create a workspace, head over to the directory on your terminal, and run the command npm install -g truffle.

Now, run the command npx truffle init to fire up the development environment. You’ll notice a new file structure appears in your file explorer, something like the image below:

truffle_init

Running a Contract Test Simulation

To understand how unit testing works using the Truffle suite create a demo directory, different from your main directory, and run the command npx truffle unbox metacoin. The result of the successful run of the code should look like the image below.

creating_metacoin

The command starts up a demo project called <metacoin> including two contract files MetaCoin.sol and ConvertLib.sol in the contract directory and also has two testing files TestMetaCoin.sol and metacoin.js file in the test directory. For running unit tests on the metacoin contracts.

Now run the command npx truffle test and the result of the unit test should look exactly like the image below.

demo_testing

Truffle first compiles the contract, runs all the unit test in the test script, and returns the result of all the tests. The image above shows the result when it passes all the unit tests.

Creating the Smart Contract

Each contract test made is composed explicitly to test a specific contract, meaning if you have four different contract files in an application, your application should likewise have four test scripts for testing each contract. In the following steps, you'll write a simple sample contract which you'll, later be writing a for.

Note: If you’re new to solidity and creating smart contracts, check out this tutorial to get started and understand solidity code. The tutorial above also has a couple of functions that will help you learn how to write solidity code.

  1. Head back to the initial development environment directory you created; inside the contract folder, create a new file, Sample.sol. This will be the smart contract you’ll be writing unit tests for.

  2. The Sample.sol contract will have the following functionalities:

  • a. First, the contract is created, and the variables name, and age are also created and by default, have no value.
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.17;

contract Sample {
string public name;
address public owner;


}
  • b. Next, the contract’s constructor function assigns the address of the deployer of the contract to the variable owner and assigns the string "deployer" to the name variable.

       constructor() {
    owner = msg.sender;
    name = "deployer";
    }
  • c. The next function rename accepts a string value as argument and assigns it to the variable name.

  
function rename(string memory _name) public {
name = _name;
}
  • d. The next function describe simply return the current values of the global variable, name.
  
function describe() public view returns (string memory) {
return (name);
}
  • e. Next is a modifier function ownerOnly that only allows the contract owner to call its parent function when added to any function.
    modifier ownerOnly() {
require(
msg.sender == owner,
"this function requires the owner of the contract to run"
);
_;
}
  • f. The following function changeOwner uses the previously created ownerOnly modifier to only allow the owner of the contract to change the role of the contract owner to any address by passing as an argument to the changeOwner function.
    function changeOwner(address _newOwner) public ownerOnly {
owner = _newOwner;
}
  • g. The next function deposit allows anyone to send a minimum of 1 ETH to the contract.
    function deposit() public payable {
require(
msg.value >= 0.01 * 10 ** 18,
"you need to send at least 0.01 ETH"
);
}
  • h. Finally, the last function in the Sample.sol contract allows anyone calling the contract to withdraw funds from the contract, as long as you pass in the number of tokens to withdraw as an argument. This transaction will also be terminated if the amount passed in exceeds than 10 ETH.
    function withdraw(uint256 _amount) public payable {
require(_amount <= 100000000000000000);
payable(msg.sender).transfer(_amount);
}

If you’ve completed your Sample.sol contract, Your smart contract should look exactly like the code below; You should update your contract with the code below for uniformity sake:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.17;

contract Sample {
string public name;
address public owner;

constructor() {
owner = msg.sender;
name = "deployer";
}

function rename(string memory _name) public {
name = _name;
}

function describe() public view returns (string memory) {
return (name);
}

modifier ownerOnly() {
require(
msg.sender == owner,
"this function requires the owner of the contract to run"
);
_;
}

function changeOwner(address _newOwner) public ownerOnly {
owner = _newOwner;
}

function deposit() public payable {
require(
msg.value >= 0.01 * 10 ** 18,
"you need to send at least 0.01 ETH"
);
}

function withdraw(uint256 _amount) public payable {
require(_amount <= 100000000000000000);
payable(msg.sender).transfer(_amount);
}
}

To confirm you have no existing errors in your contract, run the command npx truffle compile on your terminal, and a successful result should look like the image below.

compiling_contract

Now that you know the different functions in the Sample.sol contract and you’re familiar with what they do. Next, you’ll learn how to create a unit test script to test subsections of the contract you just made.

Writing the Unit Test Script

Now that you have created the Sample.sol contract, you can begin writing the unit tests for the contract. After completing these tests, you’ll have a basic idea of how to create unit tests for smart contracts.

A very common pattern used when writing unit tests for smart contracts is:

a. Arrange: This is where you create dummy variables that you’ll need to run units of your test cases. They can be created globally after the contract test function s created or locally within the unit test.

b. Act: Next, is the part where you run your testing functions and store the result in a variable.

c. Assert: Since you already know the correct result of the test, then you compare your expected result with the response of the test you ran. If the test returns the expected result, it passes else, the test does not pass.

Also following the format:

 describe(<"functionName">, async function () {
beforeEach(async function() {
<what should happen before each test is run>
})
it("what the test is expected to do", async function () {
const response = <what was returned>
const result = <what should be returned>;
expect(response).to.equal(result); // compares the response to the expected result
});

Next, you’ll be creating a uint-test to test your Sample.sol contract using the previous format above, and you’ll learn how to create a basic unit test script on your own:

Testing a smart contract makes it easier to identify bugs and vulnerabilities and reduces the possibility of software errors that could lead to costly exploits. In the next few steps, you will learn the basic format of how to write unit tests based on your smart contract.

  • First, head over to the migrations folder and create a script file called 1_deploy_contract.js and copy the code below into the script.
const Sample = artifacts.require("Sample");
// const MetaCoin = artifacts.require("MetaCoin");

module.exports = function (deployer) {
// deployer.deploy(Sample);
// deployer.link(Sample, SampleTest);
// deployer.deploy(SamplTest);
deployer.deploy(Sample, { gas: 1000000 });
};

The code above is created to simply deploy your Sample.sol contract. Next, navigate to the test folder and create a new test script, SampleTest.js.

  1. Firstly, you’ll need to import the contract as a variable Sample in the first line of code.
const Sample = artifacts.require("Sample");
  1. Next, you’ll need to initialize the contract test with the following code below. This contract - Sample will cover all the unit test functions that will be carried out on the named contract.
contract("Sample", (accounts) => {
})
  1. Using the describe keyword to define a specific test for each function in the contract, you can carry out multiple tests using the it keyword for a specific function. The first test constructor tests the constructor function in the contract. Copy and add the code below.
  describe("constructor", async function () {
it("should have the correct name", async () => {
const sample = await Sample.deployed();
const name = await sample.name();
assert.equal(name, "deployer");
});

it("should have the correct owner", async () => {
const sample = await Sample.deployed();
const owner = await sample.owner();
assert.equal(owner, accounts[0]);
});
});

The function has two tests with string descriptions of what each of them is meant to do. The first test check for the initialization of the name variable and checks the value of the owner variable to the address of the deployer. The test passes if the result returns as expected and reverts with an error otherwise.

Now, run the command npx truffle test, and a successful result should look like the image below.

test(2)

  1. The next unit test describes the rename and describe function from the smart contract; the function carries out a single test on the rename and describe function. The test updates the name variable's value and checks the current the variable's current value if it has been updated. Copy and add the code below.
  describe("rename & describe", async function () {
it("should be able to rename", async () => {
const sample = await Sample.deployed();
await sample.rename("new name");
const name = await sample.describe();
assert.equal(name, "new name");
});
});

Now, run the command npx truffle test and a successful result should look like the image below.

test(1)

  1. The next unit test describes the changeOwner function in the smart contract; the test first uses the right address to attempt to change the owner, which should pass successfully. And then uses another random address to change the ownership role, which is meant to be reverted. Copy and add the code below.
describe("changeOwner", async function () {
it("should change the owner", async () => {
const sample = await Sample.deployed();
await sample.changeOwner(accounts[1], { from: accounts[0] });
const owner = await sample.owner();
assert.equal(owner, accounts[1]);
});

it("should not change the owner", async () => {
const sample = await Sample.deployed();
try {
await sample.changeOwner(accounts[2], { from: accounts[1]});
} catch (error) {
assert.equal(
error.message,
"VM Exception while processing transaction: revert"
)};
});
});

Now, run the command npx truffle test and a successful result should look like the image below.

test(3)

  1. The next function tests the deposit function of the contract. The first test will verify the deposit function works correctly which allows deposits of 0.01 ETH or greater. The second test verifies that the deposit function correctly rejects deposits of less than 0.01 ETH. Copy and paste the code below.
  describe("deposit", async function () {
it("should allow deposits", async () => {
const sample = await Sample.deployed();
await sample.deposit({ value: 0.01 * 10 ** 18 });
});
it("should not allow deposits below 0.01 ETH", async () => {
const sample = await Sample.deployed();
try {
await sample.deposit({ value: 0.009 * 10 ** 18 });
assert.fail("deposit should have failed");
} catch (error) {
assert.ok(error.message.includes("revert"));
}
});
});

Now, run the command npx truffle test and a successful result should look like the image below.

test(4)

  1. This next describe function tests the withdraw function in the contract. The first test is attempting to withdraw 0.01 ether from the contract. The second test is attempting to withdraw an amount greater than the balance to ensure that the withdrawal fails. If the test fails, it will return an error message with the word revert.
  describe("withdraw", async function () {
it("should allow withdrawals", async () => {
const sample = await Sample.deployed();
await sample.withdraw(BigInt(0.01 * 10 ** 18));
});
it("should not allow withdrawals above balance", async () => {
const sample = await Sample.deployed();
try {
await sample.withdraw(BigInt(0.01 * 10 ** 18));
assert.fail("withdrawal should have failed");
} catch (error) {
assert.ok(error.message.includes("revert"));
}
});
});

Finally, run the command npx truffle test and a successful result should look like the image below.

test(5)_

After completing your test script, your code should look exactly like the one below. For uniformity, sake replaces the entire code with this code test.

const Sample = artifacts.require("Sample");

contract("Sample", (accounts) => {
describe("constructor", async function () {
it("should have the correct name", async () => {
const sample = await Sample.deployed();
const name = await sample.name();
assert.equal(name, "deployer");
});

it("should have the correct owner", async () => {
const sample = await Sample.deployed();
const owner = await sample.owner();
assert.equal(owner, accounts[0]);
});
});

describe("rename & describe", async function () {
it("should be able to rename", async () => {
const sample = await Sample.deployed();
await sample.rename("new name");
const name = await sample.describe();
assert.equal(name, "new name");
});
});
describe("changeOwner", async function () {
it("should change the owner", async () => {
const sample = await Sample.deployed();
await sample.changeOwner(accounts[1], { from: accounts[0] });
const owner = await sample.owner();
assert.equal(owner, accounts[1]);
});

it("should not change the owner", async () => {
const sample = await Sample.deployed();
try {
await sample.changeOwner(accounts[2], { from: accounts[1] });
} catch (error) {
assert.equal(
error.message,
"VM Exception while processing transaction: revert"
);
}
});
});
describe("deposit", async function () {
it("should allow deposits", async () => {
const sample = await Sample.deployed();
await sample.deposit({ value: 0.01 * 10 ** 18 });
});
it("should not allow deposits below 0.01 ETH", async () => {
const sample = await Sample.deployed();
try {
await sample.deposit({ value: 0.009 * 10 ** 18 });
assert.fail("deposit should have failed");
} catch (error) {
assert.ok(error.message.includes("revert"));
}
});
});
describe("withdraw", async function () {
it("should allow withdrawals", async () => {
const sample = await Sample.deployed();
await sample.withdraw(BigInt(0.01 * 10 ** 18));
});
it("should not allow withdrawals above balance", async () => {
const sample = await Sample.deployed();
try {
await sample.withdraw(BigInt(0.01 * 10 ** 18));
assert.fail("withdrawal should have failed");
} catch (error) {
assert.ok(error.message.includes("revert"));
}
});
});
});

Conclusion

Writing unit tests for smart contracts can help a great deal in ensuring a secure and proficient contract, by suggesting fixes and improvements after discovering errors, issues, and security vulnerabilities in your contract. You have successfully created your unit test script for a simple sample contract using truffle. Now that you understand how unit tests are written, you can move on to writing more complex test scripts for other smart contracts. You can also read about how to run the unit test for smart contracts using Truffle.

Next Steps

Here is some other tutorial article.

Unit testing with Hardhat and Celo

How to create and Test contract calls with Celo and Hardhat

About the Author

Mayowa Julius Ogungbola

A Software Engineer and technical writer who is always open to working on new ideas. I enjoy working on GitHub, and you could also find out what I tweet about and connect with me on LinkedIn

References

Here is a link to the complete tutorial sample code on my GitHub, Leave a ⭐on the repository if you find it helpful.

Go back

· 22 min read
John Fawole

Introduction

The reality is that a single individual or small group of people may not have the financial strength to fund a particular goal or agenda. This is where collective power becomes more effective as they invite more people to contribute towards the goal.

This process is known as crowdfunding. Reports have it that while the crowdfunding industry is valued at around \$1 billion in 2022, it will hit almost \$5 billion in 2025.

The most exciting fact is that more crowdfunding campaigns are run on the blockchain with the advent of blockchain technology and its innovations.

Thus, as a competent contract developer, you should know how to build, test, and deploy secure crowdfunding contracts. By the time you finish this article, you would have built your crowdfunding contract and deployed it on Celo.

Let’s jump into it.

Prerequisites​

These are the prerequisites you will need for this tutorial:

  • A fair understanding of Solidity and Javascript
  • An adequate understanding of the Celo blockchain and Hardhat framework

Requirements​

A Brief Overview of Celo

It is a good practice in blockchain in blockchain engineering to have a fair understanding of an ecosystem as a developer before attempting to build smart contracts or DApps on it.

Thus, we shall examine Celo briefly:

First of all, Celo is a self-sufficient layer-1 blockchain protocol that uses a battle-tested proof-of-stake consensus mechanism and is compatible with the Ethereum Virtual Machine. That means developers can use the same languages and frameworks used to build on Ethereum to build on Celo. The main thing that will change is switching the configuration when it is time to deploy.

But while they are both interoperable, Celo is distinct in terms of its architecture, ecological friendliness, economic model, and transaction processing technicalities.

One of the most distinguishing qualities of Celo is that it is a DeFi-focused blockchain with a unique identity system that can point a regular phone number to a wallet address.

Thus, Celo is one of the leading blockchains with the best user experience and is on track to banking those who are unbanked.

Having laid this brief foundation, let us roll our sleeves and proceed to write our contract.

Writing a Crowdfunding Contract with Solidity

The general idea behind the contract we are about to write is that people should be able to donate ERC-20 tokens to the campaign. If the benefactors can donate up to the target, the beneficiary can withdraw the funds.

But if it were to be the case that the campaign was not eventually successful, the smart contract would automatically refund the benefactors.

Let us go over it step-by-step:

## Step 1: Importing the IERC-20 Dependency
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./IERC20.sol";

The first thing we did was to specify the license of the contract to MIT; this is the license that allows you to use this for educational and general open-source purposes.

Then we declared the version of Solidity compiler we want to use. We imported an IERC20 interface. For that purpose, please create another file named IERC20.sol and paste this interface:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

interface IERC20 {
function allowance(address owner, address spender)
external
view
returns (uint256 remaining);

function approve(address spender, uint256 value)
external
returns (bool success);

function balanceOf(address owner) external view returns (uint256 balance);

function decimals() external view returns (uint8 decimalPlaces);

function decreaseApproval(address spender, uint256 addedValue)
external
returns (bool success);

function increaseApproval(address spender, uint256 subtractedValue)
external;

function name() external view returns (string memory tokenName);

function symbol() external view returns (string memory tokenSymbol);

function totalSupply() external view returns (uint256 totalTokensIssued);

function transfer(address to, uint256 value)
external
returns (bool success);

function transferFrom(
address from,
address to,
uint256 value
) external returns (bool success);
}

Step 2: The Campaign and Benefactors Struct

struct Campaign{
address payable beneficiary;
uint moneyRaised;
uint target;
uint beginning;
uint ending;
bool withdrawn;
Benefactors[] benefactorsInfo;
}

struct Benefactors {
address benefactor;
uint amount;
}

Just like other cousin languages, struct which is a variable container is also available in Solidity. For this contract, we created two separate structs for packing two sets of details.

Foremost, we declared a struct for the Campaign itself. Here, we inputted the address of the beneficiary, the money that has been raised so far, and the target of the campaign.

For the sake of time, we put in variables to track the beginning and end of the campaign. Since we can have several counts of the campaign, we would need to ascertain if the beneficiary of a particular campaign has withdrawn or not.

That is the reason we declared a boolean data type for withdrawal. Finally, we would need to pack in the array of the entire benefactors who gave towards a campaign in the struct.

Moving on to the second struct, its use is to track those who contributed to the crowdfunding. We are tracking two details about everyone of them; their addresses and the amount each of them gave.

Step 3: The Rest of The State Variables

   IERC20 public immutable token;

mapping(uint256 => Campaign) public campaigns;

// this mapping will be useful for ERC-20 transferFrom
mapping(uint256 => mapping(address => uint256)) public trackRaisedMoney;

uint256 public campaignCount;

constructor(address _token) {
if (_token == address(0)) revert();
token = IERC20(_token);
}

We introduced the ERC-20 token to this contract. Then we created two mappings. The first simple mapping will store the instances or lots of each campaign that will be created through this contract.

On the other hand, the following nested mapping will be useful for the transferFrom function, which we will eventually work on. Since we can have more than one campaign lots, we created a variable called campaignCount to track the campaigns.

The basic thing we did in the constructor was to initialize our token address.

Step 4: The getEndDate Function

 function getEndDate(uint8 _days) private pure returns (uint256) {
if (_days < 0) revert();
return uint256(_days * 86400);

We need to create this function first because we will need it during the launch. This function contains an if statement that a hypothetical day is less than 0, the EVM should return the day the campaign ends.

Step 5: The kickOff Function

  function kickOff(
address _beneficiary,
uint256 _target,
uint8 _endingDays
) external returns (uint256) {
// do this for auto-incrementation
campaignCount++;
campaigns[campaignCount].beneficiary = payable(_beneficiary);
campaigns[campaignCount].moneyRaised = 0;
campaigns[campaignCount].target = _target;
campaigns[campaignCount].beginning = block.timestamp;
campaigns[campaignCount].ending =
campaigns[campaignCount].beginning +
getEndDate(_endingDays);
uint256 endDate = campaigns[campaignCount].ending;
campaigns[campaignCount].withdrawn = false; // because the default of bool is false

require(
endDate < block.timestamp + 30 days,
"Campaign must end in 30 days"
);

emit Start(
campaignCount,
_beneficiary,
_target,
block.timestamp,
endDate
);
return campaignCount;
}

The kickOff function, as the name implies, is the function that will launch the contract. We incremented the campaignCount and set the members of the campaign struct. While most of the settings were done in the normal way and are straightforward, pay closer attention to how we set the ending member.

We added the beginning to the result of the getEndDate function we defined earlier. After this, we put a check that the day the campaign will end must be less than or equal to 30 days.

Moving on, we emitted some events into the log and return the campaignCount.

Step 6: The Give Function

    function give(uint256 _benefactorsId, uint256 _amount) external {
require(
campaigns[_benefactorsId].moneyRaised <=
campaigns[_benefactorsId].target,
"the target is reached already"
);
require(
block.timestamp <= campaigns[_benefactorsId].ending,
"can only give when the campaign has not ended"
);

token.transferFrom(msg.sender, address(this), _amount);
campaigns[_benefactorsId].moneyRaised += _amount;
trackRaisedMoney[_benefactorsId][msg.sender] += _amount;

Campaign storage campaign = campaigns[_benefactorsId];
campaign.benefactorsInfo.push(Benefactors(msg.sender, _amount));
emit Give(_benefactorsId, msg.sender, _amount);
}

This is the function that the benefactors will use to donate money towards the success of a campaign. We put in two checks before a benefactor can donate.

First, it must be the case that the total funds that have been raised so far have not reached the desired goal. Otherwise, there will be no need for donations any longer.

Second, a benefactor can only donate when the contract has not ended.

Once a benefactor has passed these checks, they can donate any amount to the beneficiary. After making the transfer call, we incremented the total amount in the pool.

For record purposes, we push each benefactor into the benefactor's array along with their addresses and the amount they donated.

Step 7: The unGive Function

  function undoGiving(uint256 _benefactorsId, uint256 _amount) external {
require(
block.timestamp <= campaigns[_benefactorsId].ending,
"can only ungive when the campaign has not ended"
);

// check that user indeed has token balance using the TRACKTOKENRAISED MAPPING
require(
trackRaisedMoney[_benefactorsId][msg.sender] >= _amount,
"Insufficient Balance"
);

campaigns[_benefactorsId].moneyRaised -= _amount;

trackRaisedMoney[_benefactorsId][msg.sender] -= _amount;
token.transfer(msg.sender, _amount);

// to remove msg.sender from benefactors
Campaign storage campaign = campaigns[_benefactorsId];
uint256 len = campaign.benefactorsInfo.length;
for (uint256 i = 0; i < len; ++i) {
Benefactors memory person = campaign.benefactorsInfo[i];
if (person.benefactor == msg.sender) {
campaign.benefactorsInfo[i] = campaign.benefactorsInfo[len - 1];
}
}
campaign.benefactorsInfo.pop();

emit UnGive(_benefactorsId, msg.sender, _amount);
}

Well, the reality is that sometimes, the donors might change their minds and conclude to take their donation back. Thus, there should be a function for this to allow fairness.

The first requirement we put in was that no donor can get a refund once the campaign has ended. Secondly, the said benefactor must have actually donated earlier.

Then we deducted the amount from the pool and sent the money back to the donor.

Now, we need to go a step further to remove such a donor from the benefactor's array.

Thus, we will need to carry out a for loop, and for composability, we stored what should have been campaign.benefactorsInfo.length variable. Afterward, we created a logic that such an earlier investor should be the last person in the benefactors' array.

Then we called a .pop, a method that removes the last person in an array.

Step 8: The Withdrawal Function

 function withdrawal(uint256 _Id) external {
require(
campaigns[_Id].beneficiary == msg.sender,
"Error, only the beneficiary can withdraw!"
);
require(
block.timestamp > campaigns[_Id].ending,
"cannot withdraw before ending"
);

require(campaigns[_Id].moneyRaised >= campaigns[_Id].target); // should be greater than or equal to
require(!campaigns[_Id].withdrawn, "Withdrawn already"); // recall that the default of bool is false

campaigns[_Id].withdrawn = true;
token.transfer(campaigns[_Id].beneficiary, campaigns[_Id].moneyRaised);

emit Withdrawal(_Id);
}

This is a vital function in the contract. We set out four conditions that are native to withdrawing:

  • Only the beneficiary can withdraw
  • The beneficiary can only withdraw when the campaign has ended
  • The money that has been raised should have hit the target
  • The funds in the current lot of the campaign must not have been withdrawn before

Before we can make the transfer call, we have to set the boolean withdrawal time from its false default to true. Then we made a transfer call where we sent the money raised to the beneficiary.

Step 9: The Refund Function

    // if the goal of the campaign is not met, everyone who donated should be refunded
function refund(uint256 _benefactorsId) external {
require(
block.timestamp > campaigns[_benefactorsId].ending,
"cannot withdraw before ending"
);
require(
campaigns[_benefactorsId].moneyRaised <
campaigns[_benefactorsId].target
);

uint256 bal = trackRaisedMoney[_benefactorsId][msg.sender];
// reset the balance
trackRaisedMoney[_benefactorsId][msg.sender] = 0;
token.transfer(msg.sender, bal);

emit Refund(_benefactorsId, msg.sender, bal);
}

What happens to all the money that has been raised if the campaign turns out to be unsuccessful? The purpose of this function is that the donors can call it to get their funds back if it were to be the case that they did not meet the target at the end of the day.

Step 10: The Detail Getter Functions

  function checkSuccess(uint256 _campaignCount)
external
view
returns (bool success)
{
if (
campaigns[_campaignCount].moneyRaised >=
campaigns[_campaignCount].target
// should be greater than or equal to
) {
success = true;
}
}

function getContributorAmount(uint256 _benefactorsInfo)
external
view
returns (uint256)
{
// the Data of everyone who contributed to the project is stored in the trackRaisedMoney mapping
return trackRaisedMoney[_benefactorsInfo][msg.sender];
}

// this function fetches us the data of everyone who contributed to the campaign
// first their number and the addresses of each of them
function getBenefactors(uint256 _benefactorsInfo)
external
view
returns (Benefactors[] memory)
{
// the Data of everyone who contributed to the project is stored in the trackRaisedMoney mapping
// generally unhealthy to use an array

Campaign memory campaign = campaigns[_benefactorsInfo];
// return campaigns[_benefactorsInfo].length;

return campaign.benefactorsInfo; // originally not initialized

// abeg help me fix this --Done my chief!
}

The rest of the functions in this contract are getter functions for some important details:

  • The checkSuccess function checks if any particular campaign was successful functions
  • The getContributorAmount function returns the amount that each donor gave
  • The getBenefactors returns all the donors that gave towards a particular outreach

Eventually, your full code base should appear like this:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./IERC20.sol";

contract RaiseMoney {
event Start(
uint256 id,
address benefactor,
uint256 target,
uint256 beginning,
uint256 ending
);
event Give(uint256 id, address benefactors, uint256 amount);
event UnGive(uint256 id, address benefactor, uint256 amount);
event Withdrawal(uint256 id);
event Refund(uint256 id, address benefactor, uint256 balance);

// struct to pack the variables of the campaign

struct Benefactors {
address benefactor;
uint256 amount;
}

// struct to pack the variables of the campaign
struct Campaign {
address payable beneficiary;
uint256 moneyRaised;
uint256 target;
uint256 beginning;
uint256 ending;
bool withdrawn;
Benefactors[] benefactorsInfo;
}

IERC20 public immutable token;

mapping(uint256 => Campaign) public campaigns;

// this mapping will be useful for ERC-20 transferFrom
mapping(uint256 => mapping(address => uint256)) public trackRaisedMoney;

uint256 public campaignCount;

constructor(address _token) {
if (_token == address(0)) revert();
token = IERC20(_token);
}
function getEndDate(uint8 _days) private pure returns (uint256) {
if (_days < 0) revert();
return uint256(_days * 86400);
}
/*
*@dev the _beginning param in the kickOff function
* was modifed to block.timestamp
*/

function kickOff(
address _beneficiary,
uint256 _target,
uint8 _endingDays
) external returns (uint256) {
// do this for auto-incrementation
campaignCount++;
campaigns[campaignCount].beneficiary = payable(_beneficiary);
campaigns[campaignCount].moneyRaised = 0;
campaigns[campaignCount].target = _target;
campaigns[campaignCount].beginning = block.timestamp;
campaigns[campaignCount].ending =
campaigns[campaignCount].beginning +
getEndDate(_endingDays);
uint256 endDate = campaigns[campaignCount].ending;
campaigns[campaignCount].withdrawn = false; // because the default of bool is false

require(
endDate < block.timestamp + 30 days,
"Campaign must end in 30 days"
);

emit Start(
campaignCount,
_beneficiary,
_target,
block.timestamp,
endDate
);
return campaignCount;
}

function give(uint256 _benefactorsId, uint256 _amount) external {
require(
campaigns[_benefactorsId].moneyRaised <=
campaigns[_benefactorsId].target,
"the target is reached already"
);
require(
block.timestamp <= campaigns[_benefactorsId].ending,
"can only give when the campaign has not ended"
);

token.transferFrom(msg.sender, address(this), _amount);
campaigns[_benefactorsId].moneyRaised += _amount;
trackRaisedMoney[_benefactorsId][msg.sender] += _amount;

Campaign storage campaign = campaigns[_benefactorsId];
campaign.benefactorsInfo.push(Benefactors(msg.sender, _amount));
emit Give(_benefactorsId, msg.sender, _amount);
}

function undoGiving(uint256 _benefactorsId, uint256 _amount) external {
require(
block.timestamp <= campaigns[_benefactorsId].ending,
"can only ungive when the campaign has not ended"
);

// check that user indeed has token balance using the TRACKTOKENRAISED MAPPING
require(
trackRaisedMoney[_benefactorsId][msg.sender] >= _amount,
"Insufficient Balance"
);

campaigns[_benefactorsId].moneyRaised -= _amount;

trackRaisedMoney[_benefactorsId][msg.sender] -= _amount;
token.transfer(msg.sender, _amount);

// to remove msg.sender from benefactors
Campaign storage campaign = campaigns[_benefactorsId];
uint256 len = campaign.benefactorsInfo.length;
for (uint256 i = 0; i < len; ++i) {
Benefactors memory person = campaign.benefactorsInfo[i];
if (person.benefactor == msg.sender) {
campaign.benefactorsInfo[i] = campaign.benefactorsInfo[len - 1];
}
}
campaign.benefactorsInfo.pop();

emit UnGive(_benefactorsId, msg.sender, _amount);
}

function withdrawal(uint256 _Id) external {
require(
campaigns[_Id].beneficiary == msg.sender,
"Error, only the beneficiary can withdraw!"
);
require(
block.timestamp > campaigns[_Id].ending,
"cannot withdraw before ending"
);

require(campaigns[_Id].moneyRaised >= campaigns[_Id].target); // should be greater than or equal to
require(!campaigns[_Id].withdrawn, "Withdrawn already"); // recall that the default of bool is false

campaigns[_Id].withdrawn = true;
token.transfer(campaigns[_Id].beneficiary, campaigns[_Id].moneyRaised);

emit Withdrawal(_Id);
}

// if the goal of the campaign is not met, everyone who donated should be refunded
function refund(uint256 _benefactorsId) external {
require(
block.timestamp > campaigns[_benefactorsId].ending,
"cannot withdraw before ending"
);
require(
campaigns[_benefactorsId].moneyRaised <
campaigns[_benefactorsId].target
);

uint256 bal = trackRaisedMoney[_benefactorsId][msg.sender];
// reset the balance
trackRaisedMoney[_benefactorsId][msg.sender] = 0;
token.transfer(msg.sender, bal);

emit Refund(_benefactorsId, msg.sender, bal);
}

// to check if a particular count of fundraising has been successful
function checkSuccess(uint256 _campaignCount)
external
view
returns (bool success)
{
if (
campaigns[_campaignCount].moneyRaised >=
campaigns[_campaignCount].target
// should be greater than or equal to
) {
success = true;
}
}

function getContributorAmount(uint256 _benefactorsInfo)
external
view
returns (uint256)
{
// the Data of everyone who contributed to the project is stored in the trackRaisedMoney mapping
return trackRaisedMoney[_benefactorsInfo][msg.sender];
}

// this function fetches us the data of everyone who contributed to the campaign
// first their number and the addresses of each of them
function getBenefactors(uint256 _benefactorsInfo)
external
view
returns (Benefactors[] memory)
{
// the Data of everyone who contributed to the project is stored in the trackRaisedMoney mapping
// generally unhealthy to use an array

Campaign memory campaign = campaigns[_benefactorsInfo];
// return campaigns[_benefactorsInfo].length;

return campaign.benefactorsInfo;
}
}

Creating the ERC-20 Contract

Since we imported an IERC20 interface, we will need to create an ERC-20 contract alongside our crowdfunding contract. Create a file and name it testERC.sol.

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract FooCoin is ERC20 {
address public owner;

constructor(uint256 initialAmount) ERC20("TestERC", "TST") {
owner = msg.sender;
_mint(msg.sender, initialAmount);
}

function mintMore(uint256 _amount) public {
// require(msg.sender == owner, "Only Owner can call this fxn!");
_mint(msg.sender, _amount);
}
}

We inherited an ERC-20 interface from Open Zeppelin, and the main function we included was one that allowed us to mint tokens. Compile this contract

Compilation

Save all your contracts. Now, go to your raisemoney.sol contract and compile on the terminal with npx hardhat compile. Other things being equal, your contract should compile.

Testing of the Contract

We have to test the functions in our contract to ensure everything runs well. On this note, create a file and name it test.js. Paste this test script:

const {
loadFixture, time
} = require("@nomicfoundation/hardhat-network-helpers");
const { anyValue } = require("@nomicfoundation/hardhat-chai-matchers/withArgs");
const { expect, assert } = require("chai");
const { ethers } = require("hardhat")

describe("Deploy Raisemoney", function () {
// We define a fixture to reuse the same setup in every test. or use a beforeEach function
// We use loadFixture to run this setup once, snapshot that state,
// and reset Hardhat Network to that snapshot in every test.
async function deployRaiseMoney() {

// const ONE_ETH = 1_000_000_000_000_000_000_000;

const getThousand = ethers.utils.parseUnits("1000", "ether");
const getFiveHundred = ethers.utils.parseUnits("500", "ether");
const getTwoHundred = ethers.utils.parseUnits("200", "ether");
const getHundred = ethers.utils.parseUnits("100", "ether");
const getFifty = ethers.utils.parseUnits("50", "ether");


// Contracts are deployed using the first signer/account by default
const [owner, user1, user2, user3] = await ethers.getSigners();

const raise_Money = await ethers.getContractFactory("RaiseMoney");
const test_ERC = await ethers.getContractFactory("MobiCoin");

const testERC = await test_ERC.deploy(getThousand);
const raiseMoney = await raise_Money.deploy(testERC.address);


return { testERC, raiseMoney, owner, user1, user2, user3, getThousand, getFiveHundred, getTwoHundred, getHundred, getFifty };
}

describe("Deployment", function () {

async function loadTokenfeatures() {
const { testERC, raiseMoney, owner, user1, user2, user3, getThousand, getFiveHundred, getTwoHundred, getHundred, getFifty } = await loadFixture(deployRaiseMoney);
let tx1 = await testERC.connect(user1).mintMore(getFiveHundred);
let tx2 = await testERC.connect(user2).mintMore(getFiveHundred);
let tx3 = await testERC.connect(user3).mintMore(getFiveHundred);

let ownerBalance = await testERC.balanceOf(owner.address);
let user1Bal = await testERC.balanceOf(user1.address);
let user2Bal = await testERC.balanceOf(user2.address);
let user3Bal = await testERC.balanceOf(user3.address);

return { ownerBalance, user1Bal, user2Bal, user3Bal, testERC, raiseMoney, owner, user1, user2, user3, getThousand, getFiveHundred, getTwoHundred, getHundred, getFifty };
}


it("Should mint more coins for other users", async function () {
// To load state from fixtures
const { ownerBalance, user1Bal, user2Bal, user3Bal, user4Bal, getThousand, getFiveHundred } = await loadFixture(loadTokenfeatures);


expect(ownerBalance).to.equal(getThousand);
expect(user1Bal).to.equal(getFiveHundred);
expect(user2Bal).to.equal(getFiveHundred);
expect(user3Bal).to.equal(getFiveHundred);

});

it("Should set the right token address", async function () {
// loading from fixtures...
const { testERC, raiseMoney } = await loadFixture(loadTokenfeatures);

expect(await raiseMoney.token()).to.equal(testERC.address);
});

it("should be able to kickoff properly", async function () {
const { owner, user1, user2, raiseMoney, getTwoHundred } = await loadFixture(deployRaiseMoney);

let tx1 = await raiseMoney.connect(owner).kickOff(user1.address, getTwoHundred, 29);
let campaignCount = await raiseMoney.campaignCount();
expect(campaignCount).to.equal(1);

// To test an error due to the condition "Campaign must end in 30 days"
await expect(raiseMoney.connect(user2).kickOff(user2.address, getTwoHundred, 31)).to.be.reverted;
});
// console.log("Kickoff works as expected!");

it("Should be able to give properly", async function () {
const { owner, user1, user2, user3, raiseMoney, testERC, getFiveHundred, getTwoHundred, getFifty } = await loadFixture(
loadTokenfeatures
);
await raiseMoney.connect(owner).kickOff(user1.address, getTwoHundred, 29);
// To approve the contract
await testERC.connect(user1).approve(raiseMoney.address, getFiveHundred)
await testERC.connect(user2).approve(raiseMoney.address, getFiveHundred)
await testERC.connect(user3).approve(raiseMoney.address, getFiveHundred)

await raiseMoney.connect(user1).give(1, getFifty);
await raiseMoney.connect(user2).give(1, getTwoHundred);

let [beneficiary, moneyraised] = await raiseMoney.campaigns(1);

expect(moneyraised).to.be.greaterThanOrEqual(
getTwoHundred
);

// To track the raised money of an address
let user2contributions = await raiseMoney.connect(user2).trackRaisedMoney(1, user2.address)
expect(user2contributions).to.equal(getTwoHundred);

// To test that a user can't give after target is reached
await expect(raiseMoney.connect(user2).give(1, getTwoHundred)).to.be.reverted;

// console.log(await raiseMoney.getBenefactors(1));
});

it("Should Undo giving", async function () {
const { owner, user1, user2, raiseMoney, testERC, getFiveHundred, getTwoHundred, getFifty } = await loadFixture(
loadTokenfeatures
);
await raiseMoney.connect(owner).kickOff(user1.address, getTwoHundred, 20);
// To approve the contract
await testERC.connect(user1).approve(raiseMoney.address, getFiveHundred)
await testERC.connect(user2).approve(raiseMoney.address, getFiveHundred)

// to give
await raiseMoney.connect(user1).give(1, getFifty);
await raiseMoney.connect(user2).give(1, getTwoHundred);

// to ungive
await raiseMoney.connect(user1).undoGiving(1, getFifty);

let user1contributions = await raiseMoney.trackRaisedMoney(1, user1.address);
expect(user1contributions).to.equal(0);

// It shoud fail when a user attempts to ungive beyond his balance
await expect(raiseMoney.connect(user1).undoGiving(1, getFiveHundred)).to.be.reverted;
});

it("Should Check Success after campaign target is reached", async function () {
const { owner, user1, user2, raiseMoney, testERC, getFiveHundred, getTwoHundred, getFifty } = await loadFixture(
loadTokenfeatures
);
await raiseMoney.connect(owner).kickOff(user1.address, getTwoHundred, 20);
// To approve the contract
await testERC.connect(user1).approve(raiseMoney.address, getFiveHundred)
await testERC.connect(user2).approve(raiseMoney.address, getFiveHundred)

// to give
await raiseMoney.connect(user1).give(1, getFifty);
await raiseMoney.connect(user2).give(1, getTwoHundred);
let checkSuccessBool = await raiseMoney.checkSuccess(1);
expect(checkSuccessBool).to.equal(true);
});
});

describe("Withdrawals", function () {

// Using fixures again
async function loadTokenfeatures() {
const { testERC, raiseMoney, owner, user1, user2, user3, getThousand, getFiveHundred, getTwoHundred, getHundred, getFifty } = await loadFixture(deployRaiseMoney);
let tx1 = await testERC.connect(user1).mintMore(getFiveHundred);
let tx2 = await testERC.connect(user2).mintMore(getFiveHundred);
let tx3 = await testERC.connect(user3).mintMore(getFiveHundred);

let ownerBalance = await testERC.balanceOf(owner.address);
let user1Bal = await testERC.balanceOf(user1.address);
let user2Bal = await testERC.balanceOf(user2.address);
let user3Bal = await testERC.balanceOf(user3.address);

return { ownerBalance, user1Bal, user2Bal, user3Bal, testERC, raiseMoney, owner, user1, user2, user3, getThousand, getFiveHundred, getTwoHundred, getHundred, getFifty };
}

it("Should revert with the right error if called too soon", async function () {
const { owner, user1, user2, raiseMoney, testERC, getFiveHundred, getTwoHundred, getFifty } = await loadFixture(
loadTokenfeatures
);
await raiseMoney.connect(owner).kickOff(user1.address, getTwoHundred, 20);
// To approve the contract
await testERC.connect(user1).approve(raiseMoney.address, getFiveHundred)
await testERC.connect(user2).approve(raiseMoney.address, getFiveHundred)

// to give
await raiseMoney.connect(user1).give(1, getFifty);
await raiseMoney.connect(user2).give(1, getTwoHundred);

await expect(raiseMoney.connect(user1).withdrawal(1)).to.be.revertedWith(
"cannot withdraw before ending"
);

});

it("Should revert with the right error if called from another account", async function () {
const { owner, user1, user2, raiseMoney, testERC, getFiveHundred, getTwoHundred, getFifty } = await loadFixture(
loadTokenfeatures
);
await raiseMoney.connect(owner).kickOff(user1.address, getTwoHundred, 20);
// To approve the contract
await testERC.connect(user1).approve(raiseMoney.address, getFiveHundred)
await testERC.connect(user2).approve(raiseMoney.address, getFiveHundred)

// to give
await raiseMoney.connect(user1).give(1, getFifty);
await raiseMoney.connect(user2).give(1, getTwoHundred);

await expect(raiseMoney.connect(user2).withdrawal(1)).to.be.revertedWith(
"Error, only the beneficiary can withdraw!"
);
});

it("Shouldn't fail if the unlockTime has arrived and the owner calls it", async function () {
const { owner, user1, user2, raiseMoney, testERC, getFiveHundred, getTwoHundred, getFifty } = await loadFixture(
loadTokenfeatures
);
await raiseMoney.connect(owner).kickOff(user1.address, getTwoHundred, 20);
// To approve the contract
await testERC.connect(user1).approve(raiseMoney.address, getFiveHundred)
await testERC.connect(user2).approve(raiseMoney.address, getFiveHundred)

// to give
await raiseMoney.connect(user1).give(1, getFifty);
await raiseMoney.connect(user2).give(1, getTwoHundred);

const ONE_MONTH_IN_SECS = 31 * 24 * 60 * 60;
const unlockTime = (await time.latest()) + ONE_MONTH_IN_SECS;

// Transactions are sent using the first signer by default
await time.increaseTo(unlockTime);

await expect(raiseMoney.connect(user1).withdrawal(1)).not.to.be.reverted;
});

it("Refund Benefactors tokens if target isn't met", async function () {
const { owner, user1, user2, raiseMoney, testERC, getFiveHundred, getTwoHundred, getFifty } = await loadFixture(
loadTokenfeatures
);
await raiseMoney.connect(owner).kickOff(user1.address, getFiveHundred, 20);
// To approve the contract
await testERC.connect(user1).approve(raiseMoney.address, getFiveHundred)
await testERC.connect(user2).approve(raiseMoney.address, getFiveHundred)

// to give
await raiseMoney.connect(user1).give(1, getFifty);
await raiseMoney.connect(user2).give(1, getTwoHundred);

const ONE_MONTH_IN_SECS = 31 * 24 * 60 * 60;
const unlockTime = (await time.latest()) + ONE_MONTH_IN_SECS;

// Transactions are sent using the first signer by default
await time.increaseTo(unlockTime);

await expect(raiseMoney.connect(user1).refund(1)).not.to.be.reverted;
});

});
});

Once you have compiled it, this should show on your terminal: compile and test

Now, we can move to the next phase, which is deployment to the Celo Alfajores Testnet.

Deployment of the Crowdfunding Contract to Celo

Before you plan to deploy, ensure you delete the configuration file–which is the Hardhat.config.js in your Hardhat and replace it with this:

require("@nomiclabs/hardhat-waffle");
require("dotenv").config({ path: ".env" });
require("hardhat-deploy");

// Prints the Celo accounts associated with the mnemonic in .env
task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
const accounts = await hre.ethers.getSigners();

for (const account of accounts) {
console.log(account.address);
}
});

/**
* @type import('hardhat/config').HardhatUserConfig
*/
module.exports = {
defaultNetwork: "alfajores",
networks: {
localhost: {
url: "http://127.0.0.1:7545",
},
alfajores: {
gasPrice: 1500000000,
gas: 4100000,
url: "https://alfajores-forno.celo-testnet.org",
accounts: {
mnemonic: process.env.MNEMONIC,
path: "m/44'/52752'/0'/0",
},
//chainId: 44787
},
celo: {
url: "https://forno.celo.org",
accounts: {
mnemonic: process.env.MNEMONIC,
path: "m/44'/52752'/0'/0",
},
chainId: 42220,
},
},
solidity: "0.8.0",
};

Then deploy the contract with hardhat run scripts/deploy.js --network alfajores. This was the contract address of our contract: 0xEF87eDE6F4713De16d83Ca08cf0f8A6a263Db60A.

deploy

You can always verify your contracts at Celoscan

Conclusion

Congratulations, you just learned how to build and deploy a crowdfunding contract on the Celo blockchain. This project should have enhanced your contract of not only smart contract development with Solidity but also the intricacies of successfully building on Celo.

Ensure you take your time to code along in this tutorial, and push your code to GitHub when you are done.

Next Steps

For more real-time visual testing, I recommend you test this contract on Remix, using up to 3 addresses to contribute to the crowdfunding contract.

About the Author

John Fawole is a blockchain technical writer and Solidity dev; ; connect with him on LinkedIn.

Go back

· 12 min read

header

Introduction​

In this article, we'll cover how to build an on-chain DAO with Hardhat, Solidity, and JavaScript. If you're unaware, many crypto projects are attempting to utilize DAOs (decentralized autonomous organizations) for project governance. The decentralized nature of cryptocurrency makes DAOs a popular governance model in the blockchain industry.

With this one, we're getting right into the code. The DAO's governance token will be an ERC20 token. This ERC20 token will be used to create and vote on proposals.

Prerequisites​

You must be familiar with the following to fully understand this tutorial:

  • HardHat: Hardhat is an extensible developer tool that helps smart contract developers.
  • Solidity: A high-level programming language.
  • Javascript: You should be familiar with the language's fundamentals.
  • Chai: To test smart contracts, we'll use a javascript testing package.

Requirements​

To follow along, your computer must have the most recent version of Node.js installed. Ensure Node is at least version 16.0.0 or higher if it is already installed.

Hardhat Project Setup

Create a folder called DAO and open it in VS Code. Run the following command to set up a new hardhat project in the terminal:

npx hardhat .

Remove all the files from the contracts and test folder once the hardhat setup is complete. Create the following three files: DAO.sol, Token.sol, and dao.test.js. The contracts folder should have the .sol files, and the test folder should contain the .js file.

We will need to install some dev dependencies so that the hardhat project will work properly. To install those, run the following command in the terminal:

npm install --save-dev "hardhat@^2.12.2" "@nomicfoundation/hardhat-toolbox@^2.0.0" "solidity-coverage"

We will also need to install OpenZeppelin Library as dependency for our project:

npm install "@openzeppelin/contracts"

Let’s see what these can help us with:

  • hardhat: Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
  • @nomicfoundation/hardhat-toolbox: It bundles all the commonly used packages and Hardhat plugins.
  • solidity-coverage: It is a solidity code coverage plugin for Hardhat.
  • @openzeppelin/contracts: It is a secure smart contract library for solidity

Governance Token Smart Contract

Using the ERC20 token standard, we will develop a governance token for our DAO. Open Token.sol and insert the following code:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract Token is ERC20 {

constructor() ERC20("Token", "TOKEN") {
_mint(msg.sender, 1000000);
}

}

Please go here to read more about how the above snippet works. We can now go to the smart contract for the DAO.

DAO Smart Contract

To get things going, we'll construct an empty smart contract called DAO in the DAO.sol file and import IERC20.sol from the openzeppelin library.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract DAO {

}

Constructor Definition

Now that we have a blank smart contract, we can declare a variable inside of it with the name token of type IERC20.

The next step is creating the smart contract constructor, which takes the token's address as an argument. We keep the token address as a variable because we want our DAO contract to work with any ERC20 token. We will initialize the Token variable inside the constructor by assigning a value using the IERC20 interface and handing it an argument of the contract address.

contract DAO {

IERC20 public token;

constructor(address _token) {
token = IERC20(_token);
}
}

Defining Proposal

The option to propose and vote on proposals will be available to anyone who has a governance token and chooses to participate in the governance process. We must outline the structure of those proposals. A struct will define the format for each proposal we will create named Proposal.

    struct Proposal {
uint256 accept;
uint256 reject;
uint256 abstain;
uint256 deadline;
bytes32 title;
mapping(address => bool) voted;
}

Note: Everything other than functions goes above the constructor, and all the functions go below the constructor. Just a tip on organizing smart contracts

Storing Created Proposals

We need to store any proposal that is made. By creating a mapping called proposals, we will achieve this. The proposal will serve as the value, and the proposal's index, which we will track using the proposalIndex variable, will be the key in the mapping.

    uint256 public proposalIndex;
mapping(uint256 => Proposal) public proposals;

Create Proposal

After putting up everything related to proposals, there is just a function left that allows us to construct proposals. Starting off, let's define the function createProposal with visibility as public. This function returns the index of the created proposal (type uint256) and accepts the title (type bytes32) argument.

To access the proposals mapping with the current value of proposalIndex, we will first create a variable named proposal (with type Proposal and data location of storage). Then, by setting the value of the title (the function argument) and the current block timestamp plus one day, we will set the title and the deadline for the proposal we had gotten from the mapping. Finally, add 1 to proposalIndex's value and return the index of the proposal you just created.

    function createProposal(bytes32 _proposal) public returns(uint256) {
Proposal storage proposal = proposals[proposalIndex];

proposal.title = _proposal;
proposal.deadline = block.timestamp + 1 days;

proposalIndex++;

return proposalIndex - 1;
}

There is a problem; we only want governance token holders to have access to this service; we don't want anyone else to be able to create proposals. This can be resolved by introducing a modifier called onlyTokenHolders that checks to see if the caller's balance of their governance token is greater than zero and rejects the call otherwise. We can check the balance by using the token variable's balanceOf method.

    modifier onlyTokenHolders() {
require(
token.balanceOf(msg.sender) > 0,
"NOT_A_TOKEN_HOLDER"
);
_;
}

This modifier can be added to the createProposal function.

function createProposal(bytes32 _proposal) public onlyTokenHolders returns(uint256)

Vote on the Proposal

We've made it possible to propose, and now we can write a function so that token holders can vote on proposals. Only token holders who hadn't already voted before the deadline should be allowed to cast a vote.

The index of the proposal and the type of vote the token holder must cast are the two arguments this function should accept. We must limit the type of vote to just three options with an enum named Vote: accept, reject, and abstain.

    enum Vote {
accept,
reject,
abstain
}

We will fetch the proposal with the index supplied as an argument to the voteOnProposal function. Then, using the require statement, we'll see if the deadline has not passed or if the token holder has not already cast a vote. We shall set the voted flag for that token holder to true if all conditions are met. Following that, we will increase the number of votes cast by token holders' balance of governance tokens in accordance with their votes.

    function voteOnProposal(uint256 _index, Vote vote) public onlyTokenHolders {

Proposal storage proposal = proposals[_index];

require(block.timestamp < proposal.deadline, "INACTIVE_PROPOSAL");
require(proposal.voted[msg.sender] == false, "ALREADY_VOTED");

proposal.voted[msg.sender] = true;

if (vote == Vote.accept) {
proposal.accept += token.balanceOf(msg.sender);
} else if (vote == Vote.reject) {
proposal.reject += token.balanceOf(msg.sender);
} else {
proposal.abstain += token.balanceOf(msg.sender);
}

}

Execute Proposal

When the time has passed, the proposal will be executed by emitting an event. First, let's define the event called the winner. This event will take arguments, the proposal's index, its title, and the number of votes it received.

    event winner(uint256 _index, bytes32 proposal, Vote winningVote);

We must first determine whether the proposal is active; if it is, we cannot execute it. If not, we'll look at which votes received the most and run the event appropriately.

    function executeProposal(uint256 _index)  public {

Proposal storage proposal = proposals[_index];

require(block.timestamp > proposal.deadline, "ACTIVE_PROPOSAL");

if (proposal.accept >= proposal.reject) {
if (proposal.accept >= proposal.abstain) {
emit winner(_index, proposal.title, Vote.accept);
} else {
emit winner(_index, proposal.title, Vote.abstain);
}
}
else {
if (proposal.reject >= proposal.abstain){
emit winner(_index, proposal.title, Vote.reject);
} else{
emit winner(_index, proposal.title, Vote.abstain);
}
}
}

Testing Smart Contract

To test the contract, we're going to a chai and mocha library. To ascertain how many components of the smart contract have been tested, we will also use the hardhat coverage plugin. First, let's configure the hardhat coverage plugin. The only thing left to do is add the following to hardhat.config.js, as we have already installed hardhat coverage in the beginning:

require("solidity-coverage");

To determine how much of the part has been tested, enter the following command into the terminal:

npx hardhat coverage

This is the intended result:

Test result with zero coverage of the smart contract

Write some tests right away. You should feel comfortable running tests with chai and mocha. Some basics are as follows: Tests are created within it function and are arranged using describe. Please read this if you want to learn more about testing.

We'll use the AAA writing format, which stands for Arrange, Action, and Assert. We first create the necessary conditions for the test (arrange), then we perform the activity we are testing (act), and last, we evaluate if we are getting the results we were expecting (assert).

We need to do imports from chai, ethers, and @nomicfoundation/hardhat-network-helpers in the dao.test.js file.

const {
time,
loadFixture,
} = require("@nomicfoundation/hardhat-network-helpers");
const { expect } = require("chai");
const { ethers } = require("hardhat");

Create a describe block with the title DAO in the dao.test.js file. We will put all of our test cases, and fixtures in this describe block. contractFixture, a fixture we'll make, will deploy the contracts and generate test accounts. We'll also make our first proposal and transfer some tokens to various addresses for testing.

describe("DAO", function () {

async function contractFixture() {

const accounts = await ethers.getSigners();

const TOKEN = await ethers.getContractFactory('Token');
const token = await TOKEN.deploy();

await token.deployed();

const DAO = await ethers.getContractFactory("DAO");
const dao = await DAO.deploy(token.address);

await dao.deployed();


await token.transfer(accounts[1].address, 13000);
await token.transfer(accounts[2].address, 32300);

await dao.createProposal(ethers.utils.formatBytes32String("first proposal"))

return { accounts, token, dao };
}
// ALL TESTS SHOULD GO BETWEEN START AND END LINE
// ---------START---------


// ---------END---------
})

The deployment of contracts will be tested in the first test. To test this, we will see if the DAO contract has our governance token address properly set and if 1 million governance tokens are available.

    it("Should set right contract address", async function () {
const { token, dao } = await loadFixture(contractFixture);

expect(await dao.token()).to.equal(token.address);
expect(await token.totalSupply()).to.equal(1000000);
});

Now that we can test proposal-related functions. Let's first see if the proposal is being created correctly. To do this, we'll get the first proposal and then check that all the fields are filled in with the right data.

    it('Should create correct proposal', async function () {
const { accounts, dao } = await loadFixture(contractFixture);

const proposal = await dao.proposals(0);

expect(proposal.title).to.eq(ethers.utils.formatBytes32String("first proposal"));
expect(proposal.accept).to.eq(0);
expect(proposal.reject).to.eq(0);
expect(proposal.abstain).to.eq(0);
})

We can go ahead and check the voting function. We will load the fixture, make a proposal, and then vote on it to see if the number of votes increases due to our voting.

    it('Should have correct accept votes', async function () {
const { accounts, dao } = await loadFixture(contractFixture);

await dao.connect(accounts[1]).voteOnProposal(0, 0);
await dao.connect(accounts[2]).voteOnProposal(0, 0);

const proposal = await dao.proposals(0);

expect(proposal.accept).to.eq(45300);
})

We can check to see if someone is voting twice because it is not permitted. Voting twice should result in an error.

    it('Should revert with already voted', async function () {
const { accounts, dao } = await loadFixture(contractFixture);

await dao.voteOnProposal(0, 0);

await expect(dao.voteOnProposal(0, 0)).to.be.rejectedWith("ALREADY_VOTED");
})

The contract specifically forbids voting past the deadline, so now is a good time to put that condition to the test. With the help of the time module we imported from @nomicfoundation/hardhat-network-helpers, we may increase the block timestamp. We will vote after increasing the timestamp, which should result in an error.

    it('Should revert with inactive proposal', async function () {
const { accounts, dao } = await loadFixture(contractFixture);

await time.increase(time.duration.days(1));

await expect(dao.connect(accounts[1]).voteOnProposal(0, 0)).to.be.revertedWith("INACTIVE_PROPOSAL");
})

We can now go on to the testing executeProposal function. The timestamp must be larger than the deadline to run the executeProposal function. To test this, we will vote on the proposal, increase the timestamp, and then test to see which event is being emitted.

    it('Should emit event for first proposal on execution with accept vote as winner', async function () {
const { dao } = await loadFixture(contractFixture);

await dao.voteOnProposal(0, 0);
await time.increase(time.duration.days(1));

await expect(dao.executeProposal(0)).to.emit(dao, "winner").withArgs(0, ethers.utils.formatBytes32String("first proposal"), 0);
})

We can use the coverage command once more to run all of the tests and see how much of the smart contract we have tested. Output should resemble the following:

Test Result for partial coverage of the smart contract

Conclusion

You have successfully learned the following things from this article:

  • The on-chain DAO's workings
  • Interaction between ERC20 tokens from other contracts
  • Using Chai and Mocha to test solidity contracts
  • Use of the solidity coverage plugin

Next Steps​

We have tested the contract, but as you can see, we didn't test it completely. As a next step, apply what you learned in this article and work to reach as close to 100% test coverage as you can.

About the Author​

Nikhil Bhintade is the author of the article. Nikhil works as a product manager. He enjoys writing about cutting-edge technology and the things he is learning. You can see his most recent work on GitHub.

References​

Here is a reference to a project that was finished with tests that had 100% coverage. To understand more about using Hardhat, you may also refer to this page.

Go back

· 20 min read
Isaac Jesse

header

Introduction

The backbone of most web3 projects, if not all, is a well thought-out smart contract architecture that handles the sensitive aspect of the application such as financial transactions, health record of patients in a hospital, game logic and so on. Being a Celo developer, it is highly necessary to have a good engineering mindset when writing smart contracts to avoid costly mistakes that can put you at the mercy of the bad actors.

Prerequisites​

We are going to examine some of the best practices, approaches and good standards for writing smart contracts that are targeted at EVM-compatible networks such as Celo. This tutorial is best suited for anyone with:

  • At least foundational knowledge of smart contract development.
  • Experience with other programming languages like C++ or Python will be an added advantage.

Requirements​

We will leverage the following technologies and tools. Be sure to follow the steps accordingly.

  • VSCode
  • Nodejs >=v14.0.0
  • Hadhat.

Note: We will not cover testing in this tutorial. For testing, please refer to this tutorial

Contents

Some of the smart contract best practices we will discuss are as follows:

  • Avoid floating pragma.
  • Modular approach.
  • Circuit breaker.
  • External contract interaction.
  • Upgradability.
  • Check, effect, interaction.
  • Adopt libraries where possible.
  • Use Speed breaks where necessary.
  • Proper use of assert(), require() and revert() functions.
  • Use modifiers properly.
  • Integer division.
  • Fallback/receive function.
  • Explicit labeling of function visibility.
  • Use interface type for accepting contract addresses.
  • Use standard interface (S).

General consideration

In programming, it is assumed there is no software that is perfect. As a web3 developer, you should try to be ahead of the bad players. That is, finding, identifying and fixing loopholes in your code before others find it. Without further ado, let’s get started.

We will write fresh match-making contracts that connect and manage the processes of marriage between two opposite sexes. It features the following subjects:

  • HusbandToBe: Will have the following properties.

    • Inlaw.
    • Wife
    • Profile: (type struct)
    age
    isMale
    Bank - e.g Cash in bank balance etc
    Property : Owned tangible assets.
    Religion: What they believe in.
    Nature: Whether they are drunkard or otherwise.
    Status: Type "Enum" showing their status as single or taken or married.
  • WifeToBe: This contract will have similar properties such as:

  • Parents: Type address.

  • Husband: address (initially set to address(0)

  • isReady: Boolean - Whether the woman is ready for marriage or not.

  • Acceptance: bytes32.

  • Criteria: struct - (Personal preference of the woman about the kind of man she wants).

  • Status: Type "Enum" showing her status whether single, taken or married.

  • Inlaw: (In this case, the wife’s parents, since in Africa, parents give their daughters to the men).

  • Bride price: Type uint256

  • Bank: address - Account to receive the bride price.

  • Wealth (Husband’s possessions).

Both parties have common attributes such as operating joint accounts together etc.

Note: This is only for tutorial purposes. It may not necessarily display the exact practice. Clone this repository, then navigate to the project folder and install the dependencies.

git clone https://github.com/bobeu/advanced-hardhat-for-celo-dev.git
cd advanced-hardhat-for-celo-dev
yarn install

After that, navigate into the contracts folder and make a new folder

cd contracts
mkdir match-making

You should have a file structure that looks like this "contracts/match-making".

Avoid floating pragma

Often, many solidity developers ignorantly or unknowingly declare a list of compiler versions (solc) in contract files using the caret symbol "^" or a range of compiler versions with ">=". Declaring a version like this "pragma solidity ^0.8.9;" tells the compiler to make a decision for you by simply selecting a version starting from 0.8.9 but not including 0.9.0. If there is no version specified in the hardhatconfig, then it defaults to the version that was preinstalled during project setup. If you declare a list or range of versions with a version declared in the config file, the compiler resolves to using a version which is specified in the configuration file. Although external libraries like the openzeppelin’s mostly adopt the float style, that's acceptable since you will mostly inherit them as they serve as base contracts and not the final deployable contracts.

The best practice is to always specify a single compiler version at the top of your contract file. Always lock your pragma to one specific tested compiler version so that your contracts are not deployed using versions that introduce undiscovered bugs.

Overview of example contracts

Before a marriage takes place, there has to be two parties who are opposite sex expressing love to each other. So create a file under the "match-making" folder named "WifeToBe.sol", then paste the following code.

 pragma solidity 0.8.9;

Modular approach

Sometimes, one may need to write more code than expected. Having code with fewer lines may be easily maintained, but the case is different with lines that run into hundreds to thousands. Breaking your code into smaller modules i.e. sub-contracts makes it easy to detect and fix bugs quickly. Also, modularity improves readability and many more advantages. In our "match-making" contract, each of the subjects will be represented by a standalone deployable contract and they can interact by interfacing with one another. Now, create contract files representing each of the subjects. Since they will have to interact with one another, then it makes sense to have a separate folder for interfaces such as a husband-to-be proposing to a lady of his choice, to the lady, acceptance is optional. Such action should forward a response to the intended party. Populate each of the files with their respective code here

  • contracts
    • match-making
      • interfaces
        • Common.sol
        • IHusbandToBe.sol
        • IWifeTobe.sol
        • IParent.sol
      • wealth
        • CashInBank.sol
        • Properties.sol
      • HusbandToBe.sol
      • WifeToBe.sol
      • Parent.sol

Our contract code size is a bit bigger and we have successfully broken them down to implement a modular style.

Circuit breaker

  • This acts as a stopper or a switch that breaks executions.

  • In the event a bug is detected in your contract or a potential vulnerability has adversely affected the dApp, you need a way to mitigate or prevent a huge loss from further happening. Halting contract execution may be very effective. Usually, this would be a function that when invoked, would simultaneously prevent execution of other functions.

  • The openzeppelin "Pausable" module is an example of a circuit breaker. When your contract inherits from it, you will need to activate it by overriding the "_pause()" and "_unpause()" functions so that only authorized accounts e.g the "owner" can call it. In our case, we will need each of the subjects to be able to have some level of control over their respective profiles soon as they subscribed in order to prevent reserved functions from being invoked by anyone. By doing so, we will have a reasonable approval mechanism among the parties that regard and respect each party’s opinion such as the WifeToBe can express her personal feelings even while the parents feel certain things are right for her.

  • In Africa, we’ve had countless cases where parents force their daughters to marry men they do not wish to marry. Having such a tamper-proof system as the Celo blockchain to balance, checkmate preferences and respect opinions would go a long way to make the world a better place. We are going to leverage the openzeppelin’s "security" and "access" modules to implement a circuit breaker that pauses the contract when invoked by the authorized account i.e. "owner". Copy/paste the following code at the top of each standalone deployable contract file.

Your files should look like these:

HusbandToBe.sol

image

WifeToBe.sol

image

Inlaw.sol renamed to Parent.sol

image

Bank.sol

image

The openzeppelin library makes available for us two internal functions to activate the breaker. They hold so much power that care must be taken who calls them.

  • pause()
  • unpause()

Paste the following code at the bottom of each of the 4 files.

   // …. 
function pause() public onlyOwner {
_pause();
}

function unpause() public onlyOwner {
_unpause();
}

We have successfully made the functions public and only authorized owner account can call them but at this point, nothing happens when they are invoked. To effect the breaker, we need invoke it on relevant functions. From the "Pausable.sol", there are two utilities that are capable of modifying functions where they are present. In our case, we only need the latter.

  • whenPaused
  • whenNotPaused

In "WifeToBe.sol", invoke the "whenNotPaused" modifier on :

  • meetYourWife()
  • tryPropose()
  • setMarriageStatus()

Example:

image

Parent.sol

  • getMarriageApproval()

When "pause()" is invoked, all functions with the "whenNotPaused" modifier will fail.

External contract interaction/External call

In solidity, contracts can be configured as standalone or dependent on other contracts. In either context, they can be made to interact with other contracts either in a high-level or low-level way. Each of these methods have their pros and cons, and the modus operandi is always to consider safety first whichever best fits your need.

High level calls are proven to be more secure compared with low-level interactions since much control is not given to the message receiver unlike its counterpart where a call from contract to another could change the flow. It is always advisable to consider a low-level call last unless you know what you’re doing. In our case, we could use low-level interaction since we own the code and can always attest to its genuineness.

A good practice is to always tag or name that contracts we do not trust as "unsafe" when making external call. Another thing to note is that low-level functions : ".call()", ".staticcall()" and ".delegatecall()" do not throw an exception when the call stack is depleted, instead, they return false. This is very unsafe and so it is mostly discouraged.

image

On Line 78, we perform a high-level callback into whoever is calling to read their profile. In this case, we expect that the caller should be a contract of type "HusbandToBe". In our case, the "_msgSender()" is a trusted party so we do not have to worry about malicious code hijacking control flow. The flow terminates as soon as the execution of "getProfile()" completes, and the result is tracked in the "_p" variable (user-defined type from an elementary type ‘struct’). Assume we are using a low-level call, we could something like:

 bytes memory _calldata = abi.encodeWithSignature("getProfile()");
(bool done, bytes memory returndata) = _msgSender().call(_calldata);

If "_msgSender()" is a bad actor, they could execute malicious code that might cause serious damages to the calling contract. As much as possible, avoid low-level calls especially with in-line assembly except if you know what you’re doing. And if you need to make an external call, Avoid state changes after the call. Avoid delegating to code you do not trust. Below is an example how "delegatecall()" can cause loss of funds and the contract destroyed.

 contract BadActor {
function performActionOnMyBehalf() external {
selfdestruct(0);
}
}

contract Enquirer {
function getProfile(address proxy) public returns(string memory) {
// Unsafe call
proxy.delegatecall(bytes4(keccak256("performActionOnMyBehalf()")))
}
}

The potential danger here is allowing users to supply an argument to the "getProfile" function. This should be discouraged.

Let’s fix an issue in the WifeToBe.sol contract.

Before

image

After

image

Upgradability

Writing 100% secure smart contracts is nearly impossible especially with complex code. The immutability attribute of blockchain is one kind that makes it stand out from other technologies. What if a potential bug that could lead to loss of millions of dollars is detected? Does it mean we will have no way to fix it? Fortunately, overtime, the developer communities have developed strategies to fix issues in contracts through smart contract upgrade which comes at a cost. However, while some upgrade methods do not alter code already deployed to the blockchain but provide subtle ways to deploy several versions of the same software while retaining users’ trust and confidence, others may require a complete software upgrade with a new deployment address each time upgrade is performed. From description, the disadvantages are not far fetched. Smart contract upgrade is a wide topic on its own which is not covered in this tutorial. To learn more, please refer to these link: How to create upgradeable smart contract on Celo

Check, effect, interaction

This largely depends on the design of the code and what it tends to achieve. But the concept of check, effect pattern means you make the necessary check, effect necessary changes to state variable (s) before initiating external call. Example is determining if an user is entitled to receive funds in a contract. Consider the following code:


1 // SPDX-License-Identifier: MIT
2
3 pragma solidity 0.8.9;
4
5 contract Reward {
6 mapping(address=>uint256) public rewardBalances;
7
8 function claimReward() external returns(bool) {
9 uint bal = rewardBalances[msg.sender];
10 require(bal > 0, "No reward");
11 (bool s, ) = msg.sender.call{value: bal}("");
12 require(s, "Failed");
13 rewardBalances[msg.sender] -= bal;
14 }
15 }
16
17
18 contract BadActor {
19 Reward reward;
20 uint receiveCounter;
21
22 constructor (address rewardContract) {
23 reward = rewardContract;
24 }
25
26 receive() external payable {
27 receiveCounter ++;
28 drainBalance();
29 }
30
31 function drainBalance() public {
32 reward.claimReward();
33 }
34 }

The essence of the "claimReward" function is to send a reward to the caller if they have balances registered in their favor. Unfortunately, the bad actor is one of the beneficiaries who had submitted a contract address as sole beneficiary for the reward.

In the bad actor’s contract, he had written a function called "drainBalance" which calls back into "Reward.claimReward()" even before line 12 is reached. Here is how the flow goes:

  • The attacker invokes "drainBalance()" which then calls "Reward.claimReward()".
  • Reward extracts the caller’s balance, checks if the balance is greater than the minimum withdrawal amount.
  • It sends payment to the BadActor by calling the "receive()" function which then invokes "drainBalance" again. The circle is restarted while waiting for line 11 to finish execution. The balance of the Bad actor contract in "Reward" will always remain the same while it withdraws the same amount over and over until the balance in the Reward contract is completely drained.

An effective solution is using the check, effect, interaction pattern or the "@openzeppelin/contracts/security/ReentrancyGuard.sol" module.

1    // SPDX-License-Identifier: MIT
2
3 pragma solidity 0.8.9;
4
5 contract Reward {
6 mapping(address=>uint256) public rewardBalances;
7
8 function claimReward() external returns(bool) {
9 // check
9 uint bal = rewardBalances[msg.sender];
10 require(bal > 0, "No reward");
11
12 // effect
13 rewardBalances[msg.sender] -= bal;
14
15 // interaction
16 (bool s, ) = msg.sender.call{value: bal}("");
17 require(s, "Failed");
18
19 }
20 }

In "Bank.sol", we implement this pattern. image

Adopt libraries where possible

While writing your own code is encouraged, it's a good practice leveraging battle-tested code such as the openzeppelin library. Even at that, merely inheriting from the library is not enough, you should have full understanding of any of the contract modules you are using as base contract. From the image below, we import a few modules from the openzeppelin library.

image

Use speed break where necessary

Using method (s) that delays action for a set period of time before execution is finalized can be very helpful. It is useful in situations where the unplanned happens so there is enough time to react and/or recover loss (es) that should have occurred. For example where a substantial amount of funds is involved, you could set a waiting period of say 14 days before withdrawal is effected so there is room for action to be taken in event of an authorized action being performed. Below on line 73, we implemented a speed break.

image

Proper use of assert(), require() and revert()

Use "assert()" function only for internal conditional test and check invariants such as comparing the balance in a contract after a transaction has been effected. So this means that a properly effected transaction should never reach the assert code. The "assert()" statement should never be used to validate input.

The "require()" function should be used to validate conditions that must evaluate to true such as checking inputs from users. For example, if you’re expecting users to supply a number between 1 and 255, you should either use the "require" function or an "if" statement with "revert" function. Lines 101, 102 and many other places show how we implement "require()" and "revert()".

image

Use modifiers properly

Modifiers work similar to functions except that they don’t have visibility and the code inside modifiers are checked and executed before proceeding to the function body. Be careful when making an external call inside a modifier as it can lead to reentrancy attack. A proper use of modifier is to make it run a condition check that should have been duplicated in many places but where you have one conditional check, you should use either "require()" or "revert()" statement. The essence of this is to make your code more readable and avoid redundancy.

image

"isPartnerAOrB"** function modifier contains code used in almost all the functions, so it makes a lot of sense to have them in a modifier and simply reference them by a variable "isPartnerAOrB".

Integer division

Solidity does not provide a proper way to account for remainder when making integer division e.g "x = 9 / 4". All integer divisions always round down to the nearest integer and the remainder is lost. This is not ideal especially when dealing with payment split or computation that has to do with funds. If you need to achieve precision, you should consider using a multiplier but be mindful that any number you use as a multiplier must be accounted for in the future when dealing with the result. Alternatively, you could switch to using percentage if you can quantify the denominator in percentage. Use a multiplier to avoid rounding to zero.

image

Fallback/receive function

In solidity, fallback function as the name implies is similar to an escape function which is reserved for an event where the smart contract is called with an empty data or a function that does not exist in the contract is invoked. Fallback is used in version of solidity before 0.6.x in the following scenarios: Ether is sent to a contract with no data. Data is sent to a contract but no match for function in the data.

Sending ether to a contract without data via ".transfer()" or ".send()" triggers the contract’s fallback function. To receive ether, fallback must be payable. The syntax is often written as:

 fallback () external payable { }

Fallback function also serves a good use case in implementing upgradable contracts.

Example:

 pragma solidity 0. 5.4;

contract DelegateToProxy {
// contract address where the logic resides
address internal implemetationAddress;

function ( ) external payable {
// Retrieve the implementation address
address impl = implemetationAddress;

assembly {
calldatacopy(0, 0, calldatasize( ))
let result := delegatecall(gas( ), impl, 0, calldatasize( ), 0, 0)
returndatacopy(0, 0, returndatasize( ))

Switch result
case 0 { revert(0, returndatasize( )) }
default { return(0, returndatasize( )) }
}
}
}

Warning! This is for example purpose, do not use in production

From version 0.6.x an improvement was released that splitted the fallback function into two separate functions:

  • fallback() external payable { }
  • receive external payable { }

Using payable with "fallback()" is now optional, and the function is used when no other function in the contract matches the call data. It always receives data and it is declared without the "function" keyword. If you intend to only use the fallback function for logging received Ether, you should always check if the call contains any data or not.

 // …
fallback() external payable {
require(msg.data.length = = 0, "");
emit DepositConfirmed(msg.sender);
}

The receive function should be used where call data is empty and any value is sent via ".send()" or ".transfer()". A contract cannot have more than one "receive" function, and it is declared without the "function" keyword with its visibility marked as "external".

In our contracts, we also implement the receive function. As an exercise, try to implement a fallback function in each of the contracts that ensures a call to it does not include any data.

image

Explicit labeling of function visibility

You should always endeavor to explicitly label function visibility including state variables.

Different Kinds of visibility in solidity are :

  • external
  • public
  • internal
  • private

Facts about visibility in solidity

  • You cannot label state variables as private.

  • Explicit labeling corrects assumptions about who can call certain functions or access state variables.

  • Functions in the interface are always labeled "external".

  • Functions or variables marked as public can be accessed by anyone.

  • External functions cannot be called in internal functions.

  • Private variables and functions are not visible to other contracts but only contracts where they’re defined.

  • External functions may be more efficient when they receive large arrays of data.

  • Internal functions and state variables are only available internally without "this" keyword.

image

Use interface type for accepting contract addresses

When accepting a contract addresses arguments to functions, it is advisable to use the interface type rather than accepting an address type so as to enable the compiler guarantee type safety throughout the input lifecycle.

Example:

 contract Somecontract {
string openLetter = "LETTER";

function verify(string memory letter) external returns(bool) {
return letter = = openLetter;
}
}

contract Verifier {
function checkCorrectness(Somecontract addr, string memory _letter) public
{
addr.verify(_letter);
}
}

image

On line 84 we accept input of type contract for the proposer and proposedTo arguments.

Use standard interface (S)

If you’re creating token (s) for your project, ensure they follow acceptable and stable token standard by implementing corresponding token interface such as EIP20 or ERC20, EIP721

Example:

image

In the code above, we create a new ERC20 compatible token by inheriting the openzeppelin ERC20 module which already has the interface implemented.

Let’s compile the code. Run :

 npx hardhat compile

image

Deploy locally.

 npx hardhat run scripts/deploy.js

image

Deploying to Celo’s testnet (Alfajores)

 npx hardhat run scripts/deploy.js – testnet alfajores

image

Conclusion​

We learned some of the best ways to write smart contracts even though there are a couple of them we might not have talked about but you’ll mostly need the ones we discussed.

Next Steps​

The example we adopted in this tutorial is a real-world case. It can be extended to make meaningful project. As an exercise, practice what you have learned by improving the contracts. Firstly, deploy locally, then to Celo testnet. Share your deployment details with us on Celo’s discord server. Happy reading.

About the Author​

Isaac Jesse, aka Bobelr is a smart contract/Web3 developer. He has been in the field since 2018, worked as an ambassador with several projects like Algorand and so on. He has also contributed to Web3 projects.

References​

Complete tutorial source code

Go back

· 13 min read

header

🌱 Introduction

Welcome Developers, to the Step-by-Step Guide to Deploying your First Full-Stack Dapp on Celo! In this guide, we will walk you through the process of building and deploying a full-stack decentralized application (Dapp) on the Celo platform.

Celo is a decentralized platform that enables fast, secure, and scalable transactions on a global scale. It is built on top of the Ethereum blockchain and is designed to be easily accessible to developers and users alike.

Whether you are a seasoned blockchain developer or just getting started, this guide will provide the knowledge and tools you need to build and deploy your own Dapp on Celo.

So let's get started!

🗈 Prerequisites

  • A computer with an internet connection. You will need a computer with a stable internet connection to follow along with this guide.

  • Basic knowledge of programming. While we will provide step-by-step instructions, it will be helpful to have some basic knowledge of programming languages such as JavaScript and Solidity.

  • Node.js and npm installed. You will need to have Node.js and npm (the package manager for Node.js) installed on your computer. You can check if you have them installed by running the following commands in your terminal:

node -v
npm -v
  • A code editor. You will need a code editor to write and edit your code. Some popular options include Visual Studio Code and Atom.
  • A Metamask account. You will need a Metamask account to interact with the Celo blockchain from your web browser. If you don't already have one, you can create one by installing the Metamask extension for Chrome or Firefox.

⚠️ Requirements

  • Truffle: a development environment, testing framework, and asset pipeline for Ethereum
  • Node.js: a JavaScript runtime that allows you to run JavaScript on the command line
  • Yarn: a package manager for JavaScript
  • next: Next.js is a framework for building server-rendered or statically-exported React applications.
  • CeloCli - The celocli lets you interact with the Celo Protocol smart contracts.

What are NFTs?

Non-fungible tokens (NFTs) are digital assets that represent ownership of a unique item or concept. They are stored on a blockchain and cannot be exchanged for something else of equal value, like traditional currencies. NFTs are often used to represent digital art, collectibles, and other unique items and are bought and sold in online marketplaces. Their value is determined by their rarity and perceived value to collectors. NFTs provide a way to prove ownership and authenticity of digital assets and allow for the creation of scarcity in the digital world, which can increase the value of certain items. They are created using smart contracts on a blockchain platform, such as Ethereum, and are often represented as ERC-721 tokens.

Let's start building the future together!

Steps to set up the truffle project and its configs

  1. Install Node.js by following the instructions on the official website.

  2. Install Yarn by running the following command:

npm install -g yarn
  1. Install Truffle by running the following command:
yarn global add truffle
  1. Install HDWalletProvider by running the following command:
npm install @truffle/hdwallet-provider --save

image

  1. Install Celo Command Line Interface also install dotenv
npm install -g @celo/celocli
npm install dotenv
// dotenv will help us to load .env file as environment variables

  1. Create a new Truffle project by running the following command:
mkdir NFTsmartcontract
cd NFTsmartcontract
truffle init
tip

Learn more: If you are new to Truffle check out the Truffle docs.

This will create a new directory with the following structure:

NFTsmartcontract/
├── contracts/
│ └── Migrations.sol
├── migrations/
│ └── 1_initial_migration.js
├── test/
├── truffle-config.js
└── truffle.js
  1. Navigate to the truffle-config.js file in your project directory and Replace the following configuration for the Celo testnet:
const HDWalletProvider = require("@truffle/hdwallet-provider");
require("dotenv").config();
module.exports = {
contracts_directory: "./contracts",
contracts_build_directory: "./truffle_abis",
migrations_directory: "./migrations",
networks: {
local: {
host: "127.0.0.1",
port: 7545,
network_id: "*",
},
alfajores: {
provider: function () {
return new HDWalletProvider(
process.env.PRIVATE_KEY,
"https://alfajores-forno.celo-testnet.org"
);
},
network_id: 44787,
gas: 20000000, //make sure this gas allocation isn't over 20M, which is the max
},
celo: {
provider: function () {
return new HDWalletProvider(
process.env.PRIVATE_KEY,
"https://forno.celo.org"
);
},
network_id: 42220,
gas: 20000000, //make sure this gas allocation isn't over 20M, which is the max
},
},
mocha: {
// timeout: 100000
},
compilers: {
solc: {
version: "0.8.9", // Fetch exact version from solc-bin (default: truffle's version)
docker: false, // Use "0.5.1" you've installed locally with docker (default: false)
settings: {
// See the solidity docs for advice about optimization and evmVersion
optimizer: {
enabled: false,
runs: 200,
},
evmVersion: "istanbul",
},
},
},
};
  1. Create A Celo Account using Celo Cli
celocli account:new

image

  1. Create a .env File in the root directory and add PRIVATE_KEY that we got from Celocli Command or You can use PRIVATE_KEY from Metamask.
PRIVATE_KEY="62dda1a6a6ee2dasdasdsadasdassdas1e2200095661a1b1e9dsadsdsdsadasasd"
  1. Create .gitignore file

It is important to hide your mnemonic and other important files while developing applications. When using Git or GitHub, you can populate a .gitignore file with the code below to ensure you don’t accidentally publish these files.

# dependencies
/node_modules

# Mac users
.DS_Store

#hidden files
.env

Now we are done with setting up truffle

Steps for creating ERC721 Contract and Truffle Migration File

  1. Create a NFT.sol file in contracts/ folder.

  2. Add the following code to it.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract NFT is ERC721, ERC721URIStorage, Ownable {
using Counters for Counters.Counter;

Counters.Counter private _tokenIdCounter;
constructor() ERC721("NFTexample", "CELO") {}

function safeMint(string memory uri) public {
uint256 tokenId = _tokenIdCounter.current();
_tokenIdCounter.increment();
_safeMint(msg.sender, tokenId);
_setTokenURI(tokenId, uri);
}

// The following functions are overrides required by Solidity.

function _burn(uint256 tokenId) internal override(ERC721, ERC721URIStorage) {
super._burn(tokenId);
}

function tokenURI(uint256 tokenId)
public
view
override(ERC721, ERC721URIStorage)
returns (string memory)
{
return super.tokenURI(tokenId);
}
}
tip

Use the OpenZeppelin Wizard to easily create, deploy, and interact with smart contracts using the OpenZeppelin library.

  1. Install openzeppelin Library by running this command in the root folder.
npm install @openzeppelin/contracts
  1. Create a 2_deploy_contracts.js File in the migrations folder and follow the code.
const NFT = artifacts.require("NFT");
module.exports = function(deployer) {
deployer.deploy(NFT);
};

Now we are done creating NFT.sol And its Migration Config, next we gonna deploy it on Celo testnet Blockchain

Steps to deploy Smart Contract

  1. We need Faucet For deploying smart contracts on Celo Blockchain. Use Celo Testnet Faucet to get faucet money input your address which we got from celocli.

  2. Now we Gonna Compile the Smart Contract and Check if there are any problems with it.

truffle compile
  1. After successful Compilation We Now gonna deploy it on Celo Testnet
//Truffle migrate compiles AND migrates your contract. In the future, you can run truffle migrate to complete both steps but run only if you are deploying it on a Local server.

truffle migrate
// use truffle deploy --network network name to deploy on celo testnet
truffle deploy --network alfajores
//We can use other Chain as well by adding them in truffle-config.js

image

  1. After We Got our Smart Contract Address we can check it on Celo Blockchain explorer using Block Explorer.

ヾ(´⌣`)ノ Hurray we Deployed our First ERC721 Smart Contract Make Sure to Save the Smart Contract Address in a File We Gonna use it in Our Frontend. As For Our Smart Contract we have deployed is smart contract

Frontend using NextJS

Steps

  1. Set up a Next.js project:

Install Next.js and create a new Next.js project by running the following commands:

npm init next-app
cd next-app
npm run dev

image

  1. Install Ethers.js:

Ethers.js is a JavaScript library that allows you to interact with the Ethereum blockchain. To install it, run the following command in your terminal:

npm install ethers
  1. Install React-Bootstrap
npm install react-bootstrap bootstrap

image

  1. Now we have installed react-bootstrap we need to add its CSS to the \_app.js file.
import 'bootstrap/dist/css/bootstrap.min.css';

  1. Now We have our libraries which we gonna use. Now let's edit our pages/index.js file
import React from 'react';
import { ethers } from 'ethers';
import contractAbi from "./abi/NFT.json";
import { Button, Card, Container, Nav, Navbar } from 'react-bootstrap';

const contractAddress = '0xa5Dcc3EB1eC8E417A4eA6CA51bBE4119D323d6E4'; // Replace with your contract address
function App() {
const [walletAddress, setWalletAddress] = React.useState(null);

const connectToWallet = async () => {
try {
await window.ethereum.enable();
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
setWalletAddress(accounts[0]);
console.log('Wallet connected:', walletAddress);
} catch (error) {
console.error(error);
}
};

const mintNFT = async () => {
try {
const provider = new ethers.providers.Web3Provider(window.ethereum)
const signer = provider.getSigner();
const contract = new ethers.Contract(contractAddress, contractAbi.abi, signer);

// Replace with Your metadata
const metadata = 'https://ipfs.io/ipfs/QmTvsVaaHTuMNmwXgbfgkrztFEazAPyzmrb4VSS2PbqLjA?filename=the-chainlink-elf.json';

const result = await contract.safeMint(metadata, { from: walletAddress });
console.log(result);
} catch (error) {
console.error(error);
}
};

const disconnectFromWallet = async () => {
try {
await window.ethereum.request({ method: 'eth_requestAccounts', accounts: [] });
setWalletAddress(null);
console.log('Wallet disconnected');
} catch (error) {
console.error(error);
}
};
React.useEffect(() => {
const checkWalletConnection = async () => {
if (window.ethereum.isConnected()) {
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
setWalletAddress(accounts[0]);
console.log('Wallet connected:', walletAddress);
} else {
console.log('Wallet not connected');
}
};
checkWalletConnection();
}, []);
return (
<div style={{ backgroundColor: 'white' }}>
<Navbar bg="light" expand="lg">
<Navbar.Brand href="/">NFT Minter</Navbar.Brand>
<Navbar.Toggle aria-controls="basic-navbar-nav" />
<Navbar.Collapse id="basic-navbar-nav">
<Nav className="ml-auto">
{ !walletAddress ?
<Nav.Link href="#" onClick={connectToWallet}>Connect</Nav.Link> :
<Nav.Link href="#" onClick={disconnectFromWallet}>Disconnect</Nav.Link>
}
<Nav.Link href="viewnft">View NFTs</Nav.Link>
</Nav>
</Navbar.Collapse>
</Navbar>
<Container>
<Card style={{ width: '18rem' }} className="mx-auto mt-5">
<Card.Img variant="top" src={`https://ipfs.io/ipfs/QmTgqnhFBMkfT9s8PHKcdXBn1f5bG3Q5hmBaR4U6hoTvb1?filename=Chainlink_Elf.png`} />
<Card.Body>
{walletAddress && (<>
<Button variant="primary" onClick={mintNFT}>Mint NFT</Button>
</>
)}
</Card.Body>
</Card>
</Container>
</div>
);
}

export default App;
  1. We need to copy the Abi Json file from our smart contract folder truffle_abis/NFT.json and Create a new folder in our pages folder named ABI and paste it over there.

  2. Now we can run Our Command to Test it out.

npm run dev

image

ヾ(´⌣`)ノ Hurray We are done with our frontend Mint Function.

Let's Complete Our Tutorial With View NFT

Steps To Create View NFT's Page

  1. Create a new file in pages with the name viewnft.js

  2. Install axios in the project to fetch data from IPFS JSON link.

npm install axios
  1. Let's import all the files which we needed
import React from 'react';
import { ethers } from 'ethers';
import contractAbi from "./abi/NFT.json";
import { Button, Card, Container, Nav, Navbar,Row,Col } from 'react-bootstrap';
import axios from 'axios';
  1. After importing all files we gonna create functions to fetch all user nfts.
async function listTokensOfOwner() {
const provider = new ethers.providers.Web3Provider(window.ethereum)

const contract = new ethers.Contract(contractAddress, contractAbi.abi, provider);
//we are using logs to fetch users nft
const sentLogs = await contract.queryFilter(
contract.filters.Transfer(walletAddress, null),
);
const receivedLogs = await contract.queryFilter(
contract.filters.Transfer(null, walletAddress),
);

const logs = sentLogs.concat(receivedLogs)
.sort(
(a, b) =>
a.blockNumber - b.blockNumber ||
a.transactionIndex - b.TransactionIndex,
);

const owned = new Set();

for (const log of logs) {
const { from, to, tokenId } = log.args;

if (addressEqual(to, walletAddress)) {
owned.add(tokenId.toString());
} else if (addressEqual(from, walletAddress)) {
owned.delete(tokenId.toString());
}
}

const uri = [];
for (const own of owned) {
const tokenuri = await tokenUri(own);
const response = await axios.get(tokenuri);

uri.push(response.data)
}
setuserNFT(uri);
};
async function tokenUri(id){
//this function is to fetch tokenUri from smart contract
const provider = new ethers.providers.Web3Provider(window.ethereum)
const contract = new ethers.Contract(contractAddress, contractAbi.abi, provider);
const url =await contract.tokenURI(id);
return url.toString()
}
function addressEqual(a, b) {
//this functoin is for checking the address match because sometime metamask and our input wallet addresses are in different Cases.
return a.toLowerCase() === b.toLowerCase();
}
  1. Let's look at our Complete `viewnft.js`` How it looks.
import React from 'react';
import { ethers } from 'ethers';
import contractAbi from "./abi/NFT.json";
import { Button, Card, Container, Nav, Navbar,Row,Col } from 'react-bootstrap';
import axios from 'axios';

const contractAddress = '0xa5Dcc3EB1eC8E417A4eA6CA51bBE4119D323d6E4'; // Replace with your contract address
function App() {
const [walletAddress, setWalletAddress] = React.useState(null);
const [userNFt,setuserNFT] = React.useState(null);
const connectToWallet = async () => {
try {
await window.ethereum.enable();
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
setWalletAddress(accounts[0]);
console.log('Wallet connected:', walletAddress);
} catch (error) {
console.error(error);
}
};

const disconnectFromWallet = async () => {
try {
await window.ethereum.request({ method: 'eth_requestAccounts', accounts: [] });
setWalletAddress(null);
console.log('Wallet disconnected');
} catch (error) {
console.error(error);
}
};