Skip to main content

155 posts tagged with "celosage"

View All Tags
Go back

· 15 min read
Rasheed Mudasiru

header

Introduction

It is impossible to overstate the importance of automation in terms of software and problem-solving to boost productivity and assure effective releases. Version control systems have made it possible for developers to work together to create solutions. adopting automation to automate the developers' code integration (continuous integration, or CI), and promoting early release, whether it be on the Google Play store or Google Drive. In this article, we'll walk you through the process of setting up an automation system on GitHub to make it simple to deploy React Native DApps and publish them to Google Drive or the Playstore. The next step after creating a Celo DApp with React Native is making it accessible to users via the Play Store or Google Drive (for people without Google Play Accounts). By doing this, upgrades will take much less time and will not require tedious manual work. We'll be using a React Native application created by a Celo Sage member in this course. Check out the article in the repository here. Notice that we are not constructing a React Native DApp by ourselves. As soon as the development team has completed a successful build, it is our responsibility to make it accessible to our customers.

Prerequisite

  • YAML
  • GitHub Account
  • React Native
  • A repository of a Celo DApp built with React Native
  • GitHub Actions
  • A Celo wallet with some testnet CELO and Celo Dollars (cUSD)
  • Node.js and npm installed on your machine
  • Expo CLI installed on your machine

A beginner's-level familiarity with everything on the aforementioned list is sufficient to be able to follow along in this session.

Important Terms

GitHub Actions: Automate, customize, and execute your software development workflows right in your repository with GitHub Actions. You can discover, create, and share actions to perform any job you'd like, including CI and CD, and combine actions in a completely customized workflow. React Native: React Native combines the best parts of native development with React, a best-in-class JavaScript library for building user interfaces. DApp: A Dapp, or decentralized application, is a software application that runs on a distributed network. It's not hosted on a centralized server but instead on a peer-to-peer, decentralized network. YAML: Is a human-readable data-serialization language. It is commonly used for configuration files and in applications where data is being stored or transmitted- Wikipedia

Credentials Creation Stage

It is obvious to us that in order to protect privacy and prevent security breaches, establishing interactions between two systems (such as GitHub and Google Console) calls for extensive verification and authentication. For that reason, we ought to be able to build an automation system that can do this for us without much difficulty. To make the work easy for both beginning and advanced readers, we will set up all the necessary credentials and concentrate on the most crucial ones.

Setting up the Google Cloud Service JSON Account

A service JSON account, also known as an activity JSON account, is a JSON-based credential or set of data that enables simple access to and interaction with the Google Cloud Platform. It gives users access to the Google Play Store, the Google Drive API, and other GCP resources that are open for interaction. Here is a link to a 2-minute video that clarifies how to accomplish it. Now that the service account JSON has been downloaded successfully, we can go to the next stage in obtaining our subsequent credential.

Setting up Google Play Android and Google Drive Developer API

The following steps are essential for configuring the Google Drive API and the Google Play Android Developer API. Remember that they are extremely similar to the previously described approach for obtaining a service account JSON with just a few minor adjustments and a different option, so the video can potentially be followed by selecting the required and expected service instead, but with the same procedure. As they both require the same steps to be taken, I'm consolidating them here:

  • Step 1: Go to the Google API Console.
  • Step 2: Select your project from the drop-down menu in the top navigation bar.
  • Step 3: Click on the Dashboard button in the left-hand sidebar.
  • Step 4: Click on the + Enable APIs and Services button.
  • Step 5: Search for Google Drive API and click on it. And while creating Play API, choose it here
  • Step 6: On the + Enable button
  • Step 7: Click on the Create Credentials button.
  • Step 8: Select Service Account as the Application Type and click Continue.
  • Step 9: Enter a name for your service account and click Create.
  • Step 10: Select the Editor role and click Continue.
  • Step 11: Skip the Grant users access to this service account page and click Done.
  • Step 12: Click on the Create Credentials button again and select Service Account Key.
  • Step 13: Select your service account, select JSON as the key type, and click Create.
  • Step 14: Save the JSON file to your computer.

It is very important to keep all these mentioned JSONs safe for security purposes; anybody who gains access to all the mentioned JSONs can push an app to our platform, and the bill will be on us. For security purposes, let’s pay attention to this very point.

Add Expo Credentials Secrets

Next, you need to add the EXPO_CREDENTIALS secret to your repository. This secret is used by the GitHub workflow to authenticate with Expo and upload your app to the app store.

To add the EXPO_CREDENTIALS secret, navigate to Settings > Secrets in your repository, and then click on the "New secret" button. Enter EXPO_CREDENTIALS as the secret name, and paste your Expo credentials by creating secrets for both username is Expo_Username and password is Expo_Password as the secret value. Ensure the secret is set from GitHub security repository secrets.

Let’s Connect the two parties together (GitHub and Google Cloud)

We will use the strength of GitHub's security policy because, as was already established, these JSONs are intended to be kept as secrets. Its name is GitHub Secrets, and it was designed with the express aim of safely storing sensitive data like your API keys. Use the "secrets" function on GitHub. Your processes can utilize these secrets to authenticate with external services without disclosing your credentials. to make adding secrets and API keys and creating them simple. I indicated the following basic actions to take:

  • Step 1: Go to the GitHub repository.

  • Step 2: Go to the "Settings" tab of the same repository as shown below Settings

  • Step 3: Click on the Secrets tab in the left-hand sidebar. secrets

  • Step 4: Click on New repository secret.

key

  • Step 5: Enter the name and value of the secret. For example, you can create a secret called SERVICE_ACCOUNT_KEY and paste the contents of your JSON key file as the value. Repeat this process for the other secrets that you need, such as GOOGLE_DRIVE_ACCESS_TOKEN, GOOGLE_DRIVE_REFRESH_TOKEN, and GOOGLE_PLAY_API_ACCESS_KEY. Once you've added all the necessary secrets, you can reference them in your YAML file using the ${{ secrets.SECRET_NAME }} syntax.

That's it! With the YAML file and secrets set up, you can now automate the process of uploading your APK file to Google Drive and the Google Play Store every time you make a new release of your Celo dApp.

Before and After Look of the Repo

before after-repo

Creating the Configurations

After gaining access to numerous credentials, the automated phase begins. It is the moment to employ them in order to automate the deployment procedure and work miracles. We will be writing the appropriate scripts to enable all of the aforementioned actions, and GitHub Actions makes this process simple for us by only requiring that .github/workflows/ directories be present at the root of our React Native DApp projects.

Setting the configuration for the actions

To push GitHub Actions into action, we need to include the set of scripts needed to be executed on our repository in the directories we created. inside the workflows folder created in the .github folder before. We will create a file; it could be a YAML file or a JSON file, but in this tutorial, we will be using a YAML file. We will be naming the file deploy.yaml or deploy.yml. Both work the same way; choose a notation that works for you. For easy access, I will be dropping the whole script and explaining it line by line for your understanding.

name: Deploy to GDrive and GPlay
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: '14.x'
- name: Install dependencies
run: |
npm install
- name: Build app
run: |
expo login --username ${{secrets.Expo_Username}} --password ${{secrets.Expo_Password}}
expo build:android -t apk --no-publish
expo build:ios --no-publish
- name: Upload APK to Google Drive
uses: Peaceiris/actions-gdrive@v3
with:
service_account_json: ${{ secrets.GOOGLE_DRIVE_SERVICE_ACCOUNT }}
source: ./android/app/build/outputs/apk/release/app-release.apk
destination: /deployments/app-release.apk

- name: Deploy the app to Google Play Store
uses: r0adkll/upload-google-play@v1.2.2
with:
serviceAccountJson: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT }}
packageName: com.example.myapp
track: production
apk: ./android/app/build/outputs/apk/release/app-release.apk
releaseNotes: "Initial release”

Don't panic if you are unfamiliar with YAML; it is simply a key-value pair-based data structure, much like JSON, an array, or a list. I will be going over the most important parts of the scripts for easy understanding:

The name field gives a name to the workflow. As the project grows, we may need to automate many tasks beyond deploying to the Play Store or Google Drive; we may need to automate the integration of codes from different developers; we may need to onboard new members to the repository when they create their first issues or make their first pull request. All of this is achievable through GitHub actions. To be able to identify the task that is running or failing, we need a name for it. That is the function of the name field.

The on field specifies the events or actions that make the workflow run, in this case when there is a “push” to the main branch. It could happen when a pull request is made or during any other available event on GitHub.

The jobs field defines the jobs that the workflow will run. These are the various related tasks that need to be done. It could be a test job, a lint job, and so on, but in this case, there is only one job called deploy.

The runs-on field specifies the type of virtual environment to run the job on. In this case, we're using Ubuntu-latest. This is another aspect of GitHub Actions. As React Native is cross-platform, it can run on Android, iOS, the web, and other platforms. But only Mac users can build iOS, but with the help of GitHub Actions, we can set up a Mac virtual machine with XCode installed that can handle that for us without thinking about getting a Mac Book.

The steps field lists the individual steps that the job will run. These include

  1. Checking out the code
  2. Setting up NodeJS and React Native
  3. Install packages and dependencies
  4. Building the release APK
  5. Authenticating the service account
  6. Uploading the APK to Google Drive
  7. Publishing the APK to the Google Play Store, and many more.

Breakdown of each of these 7 steps

  • Step 1: Checking out the code: The first step is to check out the code from the GitHub repository. You can use the actions/checkout action to do this. Here's an example:
- name: Checkout code
uses: actions/checkout@v2
  • Step 2: Setting up NodeJS and React Native: Next, you need to set up NodeJS and React Native on the build machine. You can use the actions/setup-node action to do this. Here's an example:
- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: "14.x"
- name: Install React Native CLI
run: npm install -g react-native-cli
  • Step 3: Install packages and dependencies: You need to install all the packages and dependencies required for the app. You can use the npm install command for this.
- name: Install packages and dependencies
run: |
npm install
  • Step 4: Building the release APK: You need to build the release APK for the app. You can use the expo build:android command for this. Here's an example:
- name: Build release APK
run: |
expo login --username${{ secrets.Expo_Username}} --password ${{secrets.Expo_Password}}
expo build:android -t apk --no-publish
  • Step 5: Authenticating the service account: You need to authenticate the service account that will be used to upload the APK to Google Drive and publish it to the Google Play Store. You can create a JSON key for your service account in the Google Cloud Console and store it as a secret in your GitHub repository. Here's an example of how to authenticate the service account:
- name: Authenticate service account
uses: google-github-actions/setup-gcloud@master
with:
version: "290.0.1"
service_account_email: ${{ secrets.SERVICE_ACCOUNT_EMAIL }}
service_account_key: ${{ secrets.SERVICE_ACCOUNT_KEY }}
  • Step 6: Uploading the APK to Google Drive: You can use the google-github-actions/upload-to-google-drive action to upload the APK to Google Drive. Here's an example:
- name: Upload APK to Google Drive
uses: google-github-actions/upload-to-google-drive@master
with:
path_to_file: app-release.apk
mime_type: application/vnd.android.package-archive
folder_id: ${{ secrets.GOOGLE_DRIVE_FOLDER_ID }}
service_account_key: ${{ secrets.SERVICE_ACCOUNT_KEY }}
  • Step 7: Publishing the APK to the Google Play Store: Finally, you can use the google-github-actions/publish-to-google-play-store action to publish the APK to the Google Play Store. Here's an example:
- name: Publish APK to Google Play Store
uses: google-github-actions/publish-to-google-play-store@master
with:
bundle_file_path: app-release.aab
track: "internal"
service_account_key: ${{ secrets.SERVICE_ACCOUNT_KEY }}

The above is the explanation for the fields and key. It is necessary to examine some of the values to better understand them. Below are some explanations of the values of the necessary and important keys:

 expo build:android -t apk --no-publish
expo build:ios --no-publish

The above contains three keys: name, on, with a subkey under “on” that is push, which in turn has a subkey branches with a value of main. It is important to note at this point that the subkeys are values for the parent key, and they always expect their own value. The structure means that when a push is made to the main branch, it executes the upcoming jobs that we will be having. The name key has a direct value, the name of the workflow.

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: "14.x"
- name: Install dependencies
run: |
npm install
- name: Build app
run: |
expo build:android -t apk --no-publish
expo build:ios --no-publish
- name: Upload APK to Google Drive
uses: Peaceiris/actions-gdrive@v3
with:
service_account_json: ${{ secrets.GOOGLE_DRIVE_SERVICE_ACCOUNT }}
source: ./android/app/build/outputs/apk/release/app-release.apk
destination: /deployments/app-release.apk
- name: Deploy app to Google Play Store
uses: r0adkll/upload-google-play@v1.2.2
with:
serviceAccountJson: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT }}
packageName: com.example.myapp
track: production
apk: ./android/app/build/outputs/apk/release/app-release.apk
releaseNotes: "Initial release"

The jobs key has a single direct subkey (value), and it is the deploy job. In this section, we need a series of actions and tasks to deploy. For us to be able to deploy, we need a virtual environment to run our codes, just as we will do on our local machine. We are using ubuntu-latest here, which is the runs-on key function. followed by a series of steps, and each step is given a name for easy tracing in case of any issues or updates. The first thing to do is check out the code from the GitHub repository on the virtual machine we are running on. This is abstracted away by a package (actions/checkout@v2) built by the GitHub action team with the help of the uses key. This will be repeated for every task and job if it requires more than one job, as in our case.

The with specifies the dependencies that are required and needed to execute the defined task at that level. with the help of packages that can all be gotten from the GitHub Actions Marketplace. And lastly, let’s talk about accessing secrets in our YAML file. To do this, we will be using the keyword “secrets” with a dot (.) and attaching the name of the key we use in creating the GitHub secrets in one of the sections above.

Summarily, the whole sample code can be gotten from the marketplace, and we can modify it to suit our goals and what we want to achieve, and a closer look will reveal how some React Native commands that we normally execute in our local machine are executed to make this possible.

Conclusion

After successfully following the tutorial as it is on this repository, I believe you’ve been able to deploy your DApp to both Google Drive and the Google Play Store, and apart from that, you’ve been able to learn YAML and how to use YAML to configure the GitHub Actions to kick start the process of deployment through automation.

About the author

My name is Rasheed Mudasiru, and I'm a software developer, passionate about community building based in Nigeria. I have recently been experimenting with DevOps and blockchain promotions.

Go back

· 10 min read

header

Introduction

Securing community funds is essential in the world of cryptocurrencies. Using a hardware wallet and a multi-sig wallet is one efficient way to accomplish this. Storing large cryptocurrency holdings in a single wallet comes with inherent risks, but using a multi-sig wallet adds an extra layer of protection. In this tutorial, we'll review the benefits of securing community funds and the advantages of combining a hardware wallet with a multi-sig wallet.

Prerequisites

Readers should have a fundamental understanding of Celo and be acquainted with hardware wallets before beginning this tutorial. You will more likely follow along with the technical content and complete this tutorial if you have these prerequisites.

Requirements

To complete this tutorial, you need a basic knowledge of Celo and hardware wallets.

What is a Hardware Wallet?

A hardware wallet is a specialized tool to keep private keys safe and secure offline. This makes it extremely difficult for malware or hackers to access your keys and take your belongings. You can access assets and sign transactions on a hardware wallet even without connecting to a computer, because not all hardware wallets require connecting to one. Trezor, Ledger, and KeepKey are a few well-known hardware wallets.

A Trezor Wallet A Trezor Wallet

A Ledger Wallet A Ledger Wallet

What is a Multi-Sig Wallet?

A multi-signature wallet is a cryptocurrency wallet that necessitates multiple signatures or approvals from various parties for a transaction to be carried out. This increases security and lowers the chance of theft or unauthorized access. Each party must have its own private key when setting up a multi-sig wallet and gather a certain number of signatures before a transaction can be approved. Celo Safe is a popular example.

Celo Safe is a smart contract wallet designed specifically for the Celo network. It allows users to securely manage their Celo assets, including CELO, cUSD, and other Celo-based assets, in a decentralized and non-custodial manner. Celo Safe is built on top of the Celo blockchain and leverages the security and transparency of the blockchain to ensure the safety and integrity of community funds.

By using Celo Safe to manage community funds, users can be confident that their assets are secure and that they are contributing to the growth and success of the Celo ecosystem.

Benefits of Securing Community Funds Using Multi-sig Wallet with Hardware Wallet

Security

A combination of a hardware wallet and a multi-signature wallet creates an enhanced level of security against possible theft. One perk of the multi-signature aspect is that if one signature gets into malevolent hands, it becomes ineffective without support from additional signings. Additionally, private keys are protected within hardware wallets, far from hacking or malware intrusions, to increase safety measures further.

For cryptographic equipment, the highest level of security certification remains EAL5+. The advanced security benefits of an EAL5+-certified hardware wallet go beyond just guaranteeing the safety and security of your assets. It guarantees that the wallet has been tested and evaluated to ensure its defense against tampering, hacking attempts, and malware.

Control

We can avoid mistakes by using a hardware wallet and a multi-sig wallet. There is less chance of a single person making a mistake that could lead to the loss of community funds because multiple people are involved in approving transactions. Furthermore, there is a lower possibility of unintentionally sending money to the incorrect address because the private keys are kept in a hardware wallet.

In other words, syncing these two technologies gives you total control over your finances. Only you and the authorized staff can access the community funds when using a multi-sig wallet with a hardware wallet. By doing this, you completely rule out the possibility of a third party stealing your private keys, such as an exchange or an online wallet provider.

Transparency and Accountability

Transparency and accountability are two other benefits of syncing a multi-signature wallet with a hardware wallet. There is less opportunity for fraud attempts or financial abuse when multiple people must approve transactions. This is particularly key for community funds because there may be numerous stakeholders.

Additionally, if you lose your device or forget your PIN, it contains recovery seeds—a string of words—that you can use to locate your wallet. Therefore, you can still access your money even if your hardware wallet is stolen or lost.

Choosing the Right Hardware Wallet: Your Options

USB Hardware Wallet: Hardware wallets that use USB connectivity are known as USB hardware wallets. They are a popular option for those interested in cryptocurrencies because they are small and straightforward. Ledger Nano X and Trezor One are two popular USB hardware wallet models.

Wireless Hardware Wallet: Bluetooth can link wireless hardware wallets to a computer or mobile device. Being wirelessly accessible makes them more practical to use. Devices like the Ledger Nano S and the Trezor Model T are examples of wireless hardware wallets.

Multiple-currency Hardware Wallet: Hardware wallets that support multiple cryptocurrencies are known as "multi-currency wallets," offering users of cryptocurrencies various options. They frequently accept a variety of cryptocurrencies, such as CELO, Bitcoin, Ethereum, and Litecoin. Ledger Nano X and Trezor Model T are two examples of multi-currency hardware wallets.

Mobile Hardware Wallet: Mobile hardware wallets allow users to access their cryptocurrencies while on the go. They are made to work with mobile devices. Compared to USB or wireless hardware wallets, they are usually more compact and portable. Trezor Mobile and Ledger Live Mobile are examples of mobile hardware wallets.

Card Hardware Wallet: Here is another preferred choice. Your private keys are kept in these credit-card-sized gadgets. Although they can cost more than other kinds of hardware wallets, they are sturdy and convenient to carry around. CoolWallet S is a popular example.

CoolWallet S CoolWallet S

Choosing the Right Hardware Wallet: Tips

Given the variety of options, you must identify your unique requirements and do adequate research before choosing. By following the tips given below, you'll be able to choose the best hardware wallet for your needs and feel secure knowing that your cryptocurrency is safe.

  • Figure Out Your Needs

A hardware wallet card could be the best choice if you travel frequently. A USB hardware wallet might be a better option if you're worried about security. And a mobile hardware wallet phone might be the best option if you prefer the ease of a mobile app.

  • Currency Support

Every hardware wallet can't support every cryptocurrency. Verify if the wallet you choose can store the cryptocurrency you want. It's important to remember that some wallets support several cryptocurrencies, while others only support a limited selection. To find out which currencies are supported, visit the manufacturer's website, but be aware that currency support may evolve.

  • Manufacturer's Standing and Track Record

Select a company with a good reputation that has been around for a while. Doing this assures you that you're getting a trustworthy item from a reputable business. To get a sense of the manufacturer's customer service and general reputation, you can also look up their background, read customers' reviews, and look them up on social media.

  • Security Standards

Purchase a wallet with a security rating of EAL5+. By so doing, you can rest assured your hardware wallet will work effectively. To further boost security for community funds, consider using a multi-sig wallet and your hardware wallet. With this setup, it is much more difficult for anyone to access the community funds without authorization, which necessitates multiple signatures for each transaction.

Connecting to Celo Safe and Securing your Wallet

You can confidently set up and connect your hardware wallet to Celo Safe by following the steps provided below. Remember to take the proper safety measures to protect your hardware wallet and recovery seed.

Step 1: Get Your Hardware Wallet

Getting a hardware wallet is the first step in setting it up. Several reliable companies, including Ledger, Trezor, and KeepKey, make hardware wallets.

Tip: Only buy from the manufacturer or a reputable reseller when purchasing.

Step 2: Install the Required Software

Go to the official website of your hardware wallet and download the software required for connection. Install the software and follow the prompts to set it up.

Step 3: Connect Your Hardware Wallet to Celo Safe

Open the Celo Safe app and select "Create Safe" to create a new safe or "Add existing safe" if you already have one. Click on “Connect” and choose the type of hardware wallet you have, then follow the prompts to connect it. You can also tap on the wallet icon in the top-right corner of the screen to connect your wallet directly.

Tip: Your hardware wallet should always be kept in a secure location. CoolWallet S

CoolWallet S

CoolWallet S

Step 4: Access Your Tokens Using Your Hardware Wallet

To access Celo tokens using your hardware wallet, open the Celo Safe app and select the "My Safes" tab. Your hardware wallet will be listed as one of the accounts. You can now add other people or accounts to 'My Safes' to manage community funds.

CoolWallet S

Step 5: Transfer Celo Tokens to Your Hardware Wallet

Once you have added the required people to "My Safes," the community funds can be managed by the authorization of these members to transfer and receive funds. Go to the "My Safes" tab in Celo Safe, select “Send," and enter the amount to transfer. Choose your hardware wallet as the "From" account and enter the recipient's address.

Step 6: Confirm the Transaction

On the hardware wallet of the authorized members, they will be prompted to confirm the transaction. They will verify the details, enter their PIN or passphrase, and confirm the transaction.

Tip: Always verify the address twice before sending any money there.

Step 7: Keep Your Hardware Wallet Secure

Remember to keep your hardware wallet safe and secure. Do not share your PIN or passphrase with anyone, and keep your hardware wallet in a safe place.

Tip: For maximum security, keep the firmware on your hardware wallet updated and do not divulge your recovery seed to anyone.

Conclusion

We are glad you completed our tutorial on using a multi-signature and hardware wallet to secure community funds. You now fully understand how crucial it is to keep community funds safe and secure, and you can appreciate how useful and reassuring using these two technologies can be.

Remember that syncing a multi-sig wallet with a hardware wallet can help you store your cryptocurrencies safely, manage and expand your investment portfolio, and secure community funds. Congratulations on starting the process of securing and managing community funds!

Next Steps

The next step is to learn more about cryptocurrency security. And I recommend the resources below:

About the Author

Boyejo Oluwafemi is a hardware product developer working at the intersection of hardware and blockchain technology. He’s working to leverage his wealth of experience working on several products ranging from smart health devices to sporting gadgets to deliver smart payment solutions for crypto for a more inclusive future.

References

Go back

· 16 min read
David Ikanji

header

Introduction

This tutorial will guide you through the process of creating a smart contract for a land auction on the Celo blockchain. You will learn about the concept of blockchain-based auctions and how to set up and utilize this type of application. By the end of this tutorial, you will have the knowledge and skills to develop and use your own Celo-based marketplace for conducting land auctions. Let's get started!

Prerequisite:

Before starting this tutorial, make sure you have the following:

  • A text or code editor like Remix to create and modify code.

  • A stable internet connection and a reliable web browser to access required resources and communicate with the Celo blockchain.

Requirement:

This tutorial assumes that you have a certain level of familiarity with certain topics before you begin. Specifically, it's recommended that you have a basic understanding of the following:

  • JavaScript programming.

  • Knowledge of blockchain technology and its operations

  • Some basic knowledge of solidity

What we will be building

In this tutorial, we will be building a smart contract for a land auction on the Celo blockchain. This contract will allow users to participate in blockchain-based auctions and create their own marketplace for conducting land auctions on the Celo blockchain.

The complete code:

 // SPDX-License-Identifier: MIT

pragma solidity >=0.7.0 <0.9.0;

interface IERC20Token {
function transfer(address, uint256) external returns (bool);

function approve(address, uint256) external returns (bool);

function transferFrom(
address,
address,
uint256
) external returns (bool);

function totalSupply() external view returns (uint256);

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

function allowance(address, address) external view returns (uint256);

event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(
address indexed owner,
address indexed spender,
uint256 value
);
}

contract LandAuction {
uint256 internal landsLength = 0;

address internal cUsdTokenAddress =
0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1;

struct Land {
address payable owner;
string location;
string description;
uint256 price;
uint256 sold;
bool soldStatus;
uint256 highestBid;
address payable highestBidder;
uint256 auctionEndTime;
}
mapping(uint256 => Land) private lands;

mapping(uint256 => bool) private _exists;

// check if a land with id of _index exists
modifier exists(uint256 _index) {
require(_exists[_index], "Query of a nonexistent land");
_;
}

// checks if the input data for location and description are non-empty values
modifier checkInputData(string calldata _location, string calldata _description) {
require(bytes(_location).length > 0, "Empty location");
require(bytes(_description).length > 0, "Empty description");
_;
}

function addLand(
string calldata _location,
string calldata _description,
uint256 _price,
uint256 _auctionEndTime
) public checkInputData(_location, _description) {
require(_auctionEndTime > block.timestamp, "Auction end time must be in the future");
uint256 _sold = 0;
uint256 index = landsLength;
landsLength++;
lands[index] = Land(
payable(msg.sender),
_location,
_description,
_price,
_sold,
false,
0,
payable(address(0)),
_auctionEndTime
);
_exists[index] = true;
}

function readLand(uint256 _index) public view exists(_index) returns (Land memory) {
return lands[_index];
}


function placeBid(uint256 _index) public payable exists(_index) {
require(block.timestamp < lands[_index].auctionEndTime, "Auction has ended");
require(msg.sender != lands[_index].owner, "Owner cannot place a bid");
require(msg.value > lands[_index].highestBid, "Bid must be higher than the current highest bid");
if (lands[_index].highestBid != 0) {
// if there is already a highest bid, return the previous bid amount to the previous highest bidder
require(lands[_index].highestBidder.send(lands[_index].highestBid), "Failed to return previous highest bid");
}
lands[_index].highestBid = msg.value;
lands[_index].highestBidder = payable(msg.sender);
}

function buyLand(uint256 _index) public payable exists(_index) {
require(lands[_index].auctionEndTime < block.timestamp, "Auction not ended");
require(!lands[_index].soldStatus, "Land already sold");
require(msg.sender != lands[_index].owner, "Owner cannot buy the land");

if (lands[_index].highestBid > 0) {
// transfer the highest bid amount to the previous owner
require(IERC20Token(cUsdTokenAddress).transferFrom(msg.sender, lands[_index].owner, lands[_index].highestBid), "Transfer failed");
} else {
// transfer the price to the owner if there were no bids
require(IERC20Token(cUsdTokenAddress).transferFrom(msg.sender, lands[_index].owner, lands[_index].price), "Transfer failed");
}

// update the land sold status and owner
lands[_index].sold = lands[_index].highestBid > 0 ? lands[_index].highestBid : lands[_index].price;
lands[_index].soldStatus = true;
lands[_index].owner = payable(msg.sender);
}
function cancelAuction(uint256 _index) public exists(_index) {
require(msg.sender == lands[_index].owner, "Only owner can cancel auction");
require(!lands[_index].soldStatus, "Land has already been sold");
if (lands[_index].highestBid != 0) {
require(lands[_index].highestBidder.send(lands[_index].highestBid), "Failed to return highest bid");
}
lands[_index].auctionEndTime = block.timestamp; // set auction end time to current time to end auction
}

}

You can follow or use this project as a reference to edit yours and get the required files, images e.t.c by clicking this link

To get started, you should create a new file on Remix called LandAuction.sol. The process of creating a new file on Remix can be found in the documentation, which you can refer to for guidance.(click here).

After successfully creating the new file, the following step would be to specify some statements in our smart contract.

 // SPDX-License-Identifier: MIT

pragma solidity >=0.7.0 <0.9.0;

In the provided code, we use the statement SPDX-License-Identifier: MIT to indicate that the code is licensed under the MIT License. This is achieved through the use of the SPDX (Software Package Data Exchange) identifier, which is a standard method of identifying open-source licenses.

The next line specifies the version of the Solidity programming language that our smart contract is written in. It is crucial to specify the correct version because different versions of Solidity may have distinct features and syntax, affecting the intended behavior of our code. For this particular contract, we use version 0.7.0 or later, but not beyond 0.9.0.

Afterward, we add the interface for our ERC20 token to the smart contract.

interface IERC20Token {
function transfer(address, uint256) external returns (bool);
function approve(address, uint256) external returns (bool);
function transferFrom(address, address, uint256) external returns (bool);
function totalSupply() external view returns (uint256);
function balanceOf(address) external view returns (uint256);
function allowance(address, address) external view returns (uint256);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);

The code above presents the interface for an ERC20 token in our smart contract. The interface specifies the functions that the ERC20 token must implement, which our smart contract will interact with.

  • transfer: transfers tokens from the sender's account to another account.

  • approve: approves a specific account to withdraw tokens from the sender's account.

  • transferFrom: allows the approved account to transfer tokens from the sender's account.

  • totalSupply: returns the total amount of tokens in existence.

  • balanceOf: returns the balance of tokens in a specific account.

  • allowance: returns the remaining number of tokens that an approved account can transfer from another account.

  • Transfer event: emitted when tokens are successfully transferred from one account to another.

  • Approval event: emitted when an approval event occurs, indicating that one account is authorized to withdraw from another account.

By including this interface, we enable our smart contract to interact with any ERC20 token that implements these functions. This means that our smart contract can transfer, approve, and transferFrom tokens for any ERC20 token that follows this standard.

Next, we will name our smart contract and declare a new structure.

contract LandAuction {
uint256 internal landsLength = 0;

address internal cUsdTokenAddress =
0x874069Fa1Eb16D44d622F2e0Ca25eeA172369bC1;

struct Land {
address payable owner;
string location;
string description;
uint256 price;
uint256 sold;
bool soldStatus;
uint256 highestBid;
address payable highestBidder;
uint256 auctionEndTime;
}

In the provided code, we declare a smart contract named LandAuction. Within the smart contract, we define a new struct called Land, which has the following properties:

  • owner: the address of the current owner of the land, which is of type address payable.

  • location: a string that describes the location of the land.

  • description: a string that provides a description of the land.

  • price: the initial price set by the owner for the land, of type uint256.

  • sold: the amount of the land that has been sold, of type uint256.

  • soldStatus: a boolean value that indicates whether the land has been sold or not.

  • highestBid: the highest bid for the land, of type uint256.

  • highestBidder: the address of the highest bidder for the land, which is of type address payable.

  • auctionEndTime: the timestamp when the auction for the land ends, of type uint256.

By defining this structure, we establish a blueprint for how we will store and track information about the land in our auction system. This will allow us to keep track of important information about each land listing and manage the auction process effectively.

Next, we add our mapping and modifiers

 mapping(uint256 => Land) private lands;

mapping(uint256 => bool) private _exists;

// check if a land with id of _index exists
modifier exists(uint256 _index) {
require(_exists[_index], "Query of a nonexistent land");
_;
}

// checks if the input data for location and description are non-empty values
modifier checkInputData(string calldata _location, string calldata _description) {
require(bytes(_location).length > 0, "Empty location");
require(bytes(_description).length > 0, "Empty description");
_;
}

In this code, we define two mappings:

  • lands: a private mapping that maps a uint256 ID to a Land struct object. This mapping will be used to keep track of all the lands that are being auctioned.

  • _exists: a private mapping that maps a uint256 ID to a boolean value indicating whether the land with the corresponding ID exists. This mapping is used to ensure that we are only accessing and modifying lands that actually exist in our contract.

Additionally, we define two modifiers:

  • exists: a modifier that checks whether a land with a given ID exists in our system. If the land does not exist, the modifier throws an error.

  • checkInputData: a modifier that checks whether the input data for the location and description of a new land listing are non-empty strings. If either string is empty, the modifier throws an error.

These mappings and modifiers are crucial components of our smart contract as they help us manage the land auction process and ensure that our system is working as intended.

We will enhance the functionality of our smart contract by defining some functions. The initial function that we will define is named addLand().

 function addLand(
string calldata _location,
string calldata _description,
uint256 _price,
uint256 _auctionEndTime
) public checkInputData(_location, _description) {
require(_auctionEndTime > block.timestamp, "Auction end time must be in the future");
uint256 _sold = 0;
uint256 index = landsLength;
landsLength++;
lands[index] = Land(
payable(msg.sender),
_location,
_description,
_price,
_sold,
false,
0,
payable(address(0)),
_auctionEndTime
);
_exists[index] = true;
}

We will now define the addLand() function in our smart contract. This function will take input parameters such as the location, description, price in cUsd, and auction end time for a particular land. The function ensures that the auction end time is set in the future and creates a new Land object with the specified parameters. The object is then added to the lands mapping at the next available index, and the _exists mapping is updated to mark the index as occupied.

In addition to the addLand() function, we will also define a readLand() function.

  function readLand(uint256 _index) public view exists(_index) returns (Land memory) {
return lands[_index];
}

The readLand function is a view function, meaning that it does not modify the state of the contract and it is free to call. Its purpose is to allow anyone to read the details of a particular land that has been added to the marketplace.

The function takes one parameter _index, which is the index of the land in the lands mapping. It first checks if the land exists by using the exists modifier, which ensures that the specified index corresponds to a land that has been added to the marketplace.

If the land exists, the function returns the details of the land as a Land struct. This includes the owner's address, the location and description of the land, the price, whether or not it has been sold, the highest bid, the address of the highest bidder, and the auction end time.

By using the readLand function, anyone can query the details of a land without having to interact with the contract in any other way.

Next, we will add the buyLand function and the placeBid function, which will enable users to purchase a land and place bid for a land.

function placeBid(uint256 _index) public payable exists(_index) {
require(block.timestamp < lands[_index].auctionEndTime, "Auction has ended");
require(msg.sender != lands[_index].owner, "Owner cannot place a bid");
require(msg.value > lands[_index].highestBid, "Bid must be higher than the current highest bid");
if (lands[_index].highestBid != 0) {
// if there is already a highest bid, return the previous bid amount to the previous highest bidder
require(lands[_index].highestBidder.send(lands[_index].highestBid), "Failed to return previous highest bid");
}
lands[_index].highestBid = msg.value;
lands[_index].highestBidder = payable(msg.sender);
}

function buyLand(uint256 _index) public payable exists(_index) {
require(lands[_index].auctionEndTime < block.timestamp, "Auction not ended");
require(!lands[_index].soldStatus, "Land already sold");
require(msg.sender != lands[_index].owner, "Owner cannot buy the land");

if (lands[_index].highestBid > 0) {
// transfer the highest bid amount to the previous owner
require(IERC20Token(cUsdTokenAddress).transferFrom(msg.sender, lands[_index].owner, lands[_index].highestBid), "Transfer failed");
} else {
// transfer the price to the owner if there were no bids
require(IERC20Token(cUsdTokenAddress).transferFrom(msg.sender, lands[_index].owner, lands[_index].price), "Transfer failed");
}

// update the land sold status and owner
lands[_index].sold = lands[_index].highestBid > 0 ? lands[_index].highestBid : lands[_index].price;
lands[_index].soldStatus = true;
lands[_index].owner = payable(msg.sender);
}

The placeBid function allows users to place a bid on a particular land. Here's how it works:

  • It first checks if the auction for the land has not ended yet by comparing the current block timestamp to the auction end time.

  • It then checks that the person placing the bid is not the owner of the land.

  • It also checks that the bid being placed is higher than the current highest bid for the land.

  • If there is already a highest bid, it returns the previous bid amount to the previous highest bidder.

Finally, it updates the highest bid and highest bidder for the land.

The buyLand function allows users to buy a particular land. Here's how it works:

  • It first checks if the auction for the land has already ended by comparing the current block timestamp to the auction end time.

  • It then checks that the land has not already been sold.

  • It also checks that the person buying the land is not the current owner of the land.

  • If there was a highest bid placed on the land, it transfers the highest bid amount from the buyer to the previous owner of the land.

  • If there were no bids, it transfers the price of the land from the buyer to the owner of the land.

Finally, it updates the sold status, sold price, and owner of the land.

Another function that we will add to the contract is called cancelAuction. This function allows the land owner to cancel an ongoing auction and returns the highest bid (if any) back to the highest bidder.

function cancelAuction(uint256 _index) public exists(_index) {
require(msg.sender == lands[_index].owner, "Only owner can cancel auction");
require(!lands[_index].soldStatus, "Land has already been sold");
if (lands[_index].highestBid != 0) {
require(lands[_index].highestBidder.send(lands[_index].highestBid), "Failed to return highest bid");
}
lands[_index].auctionEndTime = block.timestamp; // set auction end time to current time to end auction
}

}

The cancelAuction function is used to cancel an ongoing auction for a land.

The cancelAuction, allows the owner of a piece of land to cancel an ongoing auction for that land. The function takes in a parameter _index, which is the index of the land in the lands array that the owner wants to cancel the auction for.

The first line of the function checks that the land at the given index exists, which is done through the exists modifier. If the land does not exist, the function will revert.

The next line requires that the caller of the function is the owner of the land being auctioned. If the caller is not the owner, the function will revert with an error message.

The third line checks that the land has not already been sold. If the land has been sold, the function will revert with an error message.

If the highest bid for the land is not 0 (i.e., there have been bids on the land), the fourth line of the function transfers the highest bid amount back to the highest bidder. If this transfer fails for some reason, the function will revert with an error message.

Finally, the function sets the auctionEndTime for the land to the current block timestamp, effectively ending the auction.

In summary, this function allows the owner of a piece of land to cancel an ongoing auction for that land, returning the highest bid to the highest bidder if there was one.

Contract Deployment

Before deploying our smart contract on the Celo blockchain, we need to ensure that certain requirements are met. These requirements may include tasks such as compiling our smart contract code into bytecode, testing the code to ensure it functions as intended, configuring the deployment parameters such as the gas limit and network ID, setting up a wallet to hold the funds needed for deployment, and ensuring that our development environment is properly configured to connect to the desired blockchain network.

To guarantee a seamless deployment of our smart contract, it is crucial to obtain the Celo extension wallet by following the link provided. Celo Extension wallet

After downloading the Celo extension wallet, the next step would be to add funds to the wallet to pay for the gas fees required to deploy the smart contract. Celo faucet. This can be accomplished by accessing the Celo Alfojares faucet using the provided link.

After verifying that our wallet has sufficient funds, we can use the Celo plugin available in the Remix environment to deploy our smart contract on the Celo blockchain.

Conclusion

Well done on creating the smart contract for auctioning lands on the Celo blockchain! It's a remarkable achievement, and you should feel proud of the effort you put in. Keep up the great work and enjoy the rewards of your dedication! 🎉

Next step

I hope you found this tutorial informative and gained valuable knowledge from it. If you wish to continue expanding your skills and knowledge, I have compiled some helpful links below that you may find useful to explore further:

the official Celo documentation

Solidity By Example, a website with code examples for learning Solidity

OpenZeppelin Contracts, a library of secure, tested smart contract code

Solidity documentation for version 0.8.17

I hope these resources prove to be useful to you!

About the author

I'm David Ikanji, a web3 developer residing in Nigeria, and I have a strong passion for working with blockchain technology.

Go back

· 5 min read

header

Introduction

The Job Board Smart Contract is a Solidity contract that enables employers to post job openings and job seekers to browse through job openings. The contract maintains a list of job postings along with their details such as job name, job description, and salary. Employers can post jobs, and job seekers can view the list of job openings. The contract also provides the functionality to remove job posts, but only the employer who posted the job can remove the job post.

Prerequisites

To follow this tutorial, you will need the following:

  • Basic knowledge of Solidity programming language.
  • A Development Environment Like Remix.
  • The celo Extension Wallet.

SmartContract

Let's begin writing our smart contract in Remix IDE

The completed code Should look like this.

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

// Job Board Smart Contract
contract JobBoard {

struct JobPosting {
uint jobId;
address employer;
string jobName;
string jobDescription;
uint256 salary;
}
uint private jobCount = 0;

mapping(uint => JobPosting) jobs;



event JobPosted(
uint jobId,
address employer,
string jobName,
string jobDescription,
uint salary
);


// Function to post a job
function postJob(
string memory _jobName,
string memory _jobDescription,
uint256 _salary
) public {
// Create a new job
JobPosting memory job = JobPosting(
jobCount,
msg.sender,
_jobName,
_jobDescription,
_salary
);
// Store the new job in the mapping
jobs[jobCount] = job;

jobCount++;

// Emit the JobPosted event
emit JobPosted(
job.jobId,
job.employer,
job.jobName,
job.jobDescription,
job.salary
);
}

function getJobposts(uint256 _jobId)
public
view
returns (
uint,
address,
string memory,
string memory,
uint
)
{
JobPosting storage job = jobs[_jobId];
return (
job.jobId,
job.employer,
job.jobName,
job.jobDescription,
job.salary
);
}

function removeJobPost(uint _jobId) external {
require(msg.sender == jobs[_jobId].employer, "not permmited");
jobs[_jobId] = jobs[jobCount - 1];
delete jobs[jobCount - 1];
jobCount--;
}


}

breakdown

First, we declared our license and the solidity version.

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

And then we defined our smart contract JobBoard.

contract JobBoard {
// Contract code goes here
}

We will now define some variables, structs and mappings that will be used by our contract.

 struct JobPosting {
uint jobId;
address employer;
string jobName;
string jobDescription;
uint256 salary;
}

uint private jobCount = 0;

mapping(uint => JobPosting) jobs;


First we define a struct called JobPosting that represents a job posting with the following fields:

  • jobId: A unique ID for the job posting.
  • employer: The address of the employer who posted the job.
  • jobName: The name of the job.
  • jobDescription: A brief description of the job.
  • salary: The salary for the job.

The jobCount variable keeps track of the number of job postings made, and the jobs mapping maps each job ID to its corresponding JobPosting struct.

We will now define one events that will be emitted by our contract.

  event JobPosted(
uint jobId,
address employer,
string jobName,
string jobDescription,
uint salary
);

This is an event called JobPosted which is emitted whenever a job posting is added to the job board. It includes the details of the job posting like job ID, employer address, job name, job description, and salary.

let's work on the functions.

        function postJob(
string memory _jobName,
string memory _jobDescription,
uint256 _salary
) public {
JobPosting memory job = JobPosting(
jobCount,
msg.sender,
_jobName,
_jobDescription,
_salary
);
jobs[jobCount] = job;
jobCount++;
emit JobPosted(
job.jobId,
job.employer,
job.jobName,
job.jobDescription,
job.salary
);
}

This is the postJob function which allows employers to post job openings to the job board. It takes in the job name, job description, and salary as parameters.

Inside the function, a new JobPosting struct is created with the jobCount as its jobId, the msg.sender as the employer and the provided jobName, jobDescription, and salary. The new JobPosting is stored in the jobs mapping using the jobCount as the key. The jobCount is then incremented to keep track of the new job posting.

    function getJobposts(uint256 _jobId)
public
view
returns (
uint,
address,
string memory,
string memory,
uint
)
{
JobPosting storage job = jobs[_jobId];
return (
job.jobId,
job.employer,
job.jobName,
job.jobDescription,
job.salary
);
}

Next, is the getJobposts function which takes a job ID as a parameter and returns the details of the job posting associated with that ID.

Inside the function, the JobPosting struct associated with the provided jobId is retrieved from the jobs mapping and stored in a local variable job. The details of the job posting are then returned as a tuple.

    function removeJobPost(uint _jobId) external {
require(msg.sender == jobs[_jobId].employer, "not permmited");
jobs[_jobId] = jobs[jobCount - 1];
delete jobs[jobCount - 1];
jobCount--;
}

Finally, the removeJobPost function which allows employers to remove a job posting from the job board. It takes a job ID as a parameter.

Inside the function, a require statement is used to ensure that the sender of the transaction is the employer who posted the job. If the condition is not met, the function will revert with the provided error message.

Deployment

To deploy our smart contract successfully, we need the celo extention wallet which can be downloaded from here

Next, we need to fund our newly created wallet which can done using the celo alfojares faucet Here

You can now fund your wallet and deploy your contract using the celo plugin in remix.

Next Steps

I hope you learned a lot from this tutorial. Here are some relevant links that would aid your learning further.

About the author

I'm Jonathan Iheme, A full stack block-chain Developer from Nigeria.

Thank You!!

Go back

· 8 min read

header

Introduction

In this tutorial, we will be creating a Decentralized Multitoken wallet smart contract that allows users to deposit and withdraw multiple ERC20 standard tokens in a single wallet. The contract is designed to support any ERC20 token that implements the IERC20 interface. The contract will also have functionality to set the maximum idle time for an account, as well as the ability to add or remove supported tokens.

Project Repository

Prerequisites

To follow this tutorial, you will need the following:

  • Basic knowledge of Solidity programming language.
  • A Development Environment Like Remix.
  • The celo Extension Wallet.

SmartContract

Let's begin writing our smart contract in Remix IDE

The completed code Should look like this.

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

interface IERC20 {
function balanceOf(address account) external view returns (uint256);

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

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

contract MultiTokenWallet {
uint256 private _maxIdleTime = 30 minutes;

address private contractOwner;

constructor() {
contractOwner = msg.sender;
}

mapping(address => mapping(address => uint256)) private _balances;
mapping(address => bool) private _supportedTokens;
mapping(address => uint256) private _lastSeen;

event Deposit(
address indexed account,
address indexed token,
uint256 amount
);
event Withdrawal(
address indexed account,
address indexed token,
uint256 amount
);

function deposit(address token, uint256 amount) external {
require(_supportedTokens[token], "Unsupported token");
require(
IERC20(token).transferFrom(msg.sender, address(this), amount),
"Token transfer failed"
);
_balances[msg.sender][token] += amount;
_lastSeen[msg.sender] = block.timestamp;
emit Deposit(msg.sender, token, amount);
}

function withdraw(address token, uint256 amount) external {
require(_balances[msg.sender][token] >= amount, "Insufficient balance");
require(
IERC20(token).transfer(msg.sender, amount),
"Token transfer failed"
);
_balances[msg.sender][token] -= amount;
_lastSeen[msg.sender] = block.timestamp;
emit Withdrawal(msg.sender, token, amount);
}

function balanceOf(
address account,
address token
) external view returns (uint256) {
return _balances[account][token];
}

function setSupportedToken(address token, bool isSupported) external {
require(
msg.sender == owner(),
"Only owner can modify supported tokens"
);
_supportedTokens[token] = isSupported;
}

function setMaxIdleTime(uint256 idleTime) external {
require(msg.sender == owner(), "Only owner can modify max idle time");
_maxIdleTime = idleTime;
}

function owner() public view returns (address) {
return (contractOwner);
}
}

Setting up

First, we declared our license and the solidity version.

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

The next part of the code defines an interface called IERC20, which is a standard interface for ERC20 tokens. It defines three functions: balanceOf(), transfer(), and transferFrom().

And then we defined our smart contract MultiTokenWallet.

interface IERC20 {
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 value) external returns (bool);
function transferFrom(address from, address to, uint256 value) external returns (bool);
}

contract MultiTokenWallet {
// Contract code goes here
}

Variables and mappings

We will now define some variables and mappings that will be used by our contract.

    uint256 private _maxIdleTime = 30 minutes;

address private contractOwner;

constructor(){
contractOwner = msg.sender;
}

mapping(address => mapping(address => uint256)) private _balances;
mapping(address => bool) private _supportedTokens;
mapping(address => uint256) private _lastSeen;

The _maxIdleTime variable determines the amount of time that an account can remain idle before its funds can be forfeited. The contractOwner variable is used to store the address of the owner of the contract, which is set to the address of the account that deployed the contract.

The _balances mapping stores the balances of each account for each supported token. The _supportedTokens mapping is used to keep track of which tokens are supported by the wallet. And the _lastSeen mapping keeps track of the last time that an account interacted with the wallet.

Events

We will now define two events that will be emitted by our contract whenever a deposit or withdrawal is made.

    event Deposit(address indexed account, address indexed token, uint256 amount);
event Withdrawal(address indexed account, address indexed token, uint256 amount);

The Deposit event is emitted whenever a deposit is made to the wallet, and includes the account that made the deposit, the token that was deposited, and the amount that was deposited. The Withdrawal event is emitted whenever a withdrawal is made from the wallet, and includes the account that made the withdrawal, the token that was withdrawn, and the amount that was withdrawn.

Deposit function

We will now define the deposit() function, which will allow users to deposit tokens into the wallet.

     function deposit(address token, uint256 amount) external {
require(_supportedTokens[token], "Unsupported token");
require(IERC20(token).transferFrom(msg.sender, address(this), amount), "Token transfer failed");
_balances[msg.sender][token] += amount;
_lastSeen[msg.sender] = block.timestamp;
emit Deposit(msg.sender, token, amount);
}

The function takes two arguments: token, which is the address of the token being deposited, and amount, which is the amount of the token being deposited. The function first checks that the token being deposited is supported and then it transfers the token from the sender to the smart contract. It then update the account balance and the last seen of the sender address.

And finally it emits a Deposit event with the defined outputs

Withdraw function

Now let's take a look at the withdraw() function. This function allows a user to withdraw a certain amount of tokens from their balance in the contract. Similar to the deposit function, the user must specify which token they wish to withdraw, and the amount they wish to withdraw.

function withdraw(address token, uint256 amount) external {
require(_balances[msg.sender][token] >= amount, "Insufficient balance");
require(IERC20(token).transfer(msg.sender, amount), "Token transfer failed");
_balances[msg.sender][token] -= amount;
_lastSeen[msg.sender] = block.timestamp;
emit Withdrawal(msg.sender, token, amount);
}

The function first checks that the user has a sufficient balance of the specified token in the contract. If the user does not have enough balance, the function will revert and the transaction will not be executed. If the user has enough balance, the function will transfer the specified amount of tokens from the contract to the user's address using the transfer function from the IERC20 interface. The user's balance in the contract is then updated accordingly and the _lastSeen mapping is updated with the current timestamp to track the last time the user interacted with the contract. Finally, the function emits a Withdrawal event to notify external parties of the transaction.

BalanceOf function

Next, let's look at the balanceOf function:

function balanceOf(address account, address token) external view returns (uint256) {
return _balances[account][token];
}

This function simply returns the balance of the specified token for the specified account. It is a view function, meaning that it does not modify the state of the contract and does not require a transaction to be executed.

setSupportedToken function

Moving on, we have the setSupportedToken function:

  function setSupportedToken(address token, bool isSupported) external {
require(msg.sender == owner(), "Only owner can modify supported tokens");
_supportedTokens[token] = isSupported;
}

This function allows the contract owner to add or remove support for a particular token. The function takes in the token address and a boolean flag indicating whether the token should be supported or not. The function first checks that the caller of the function is the contract owner using the owner function, and reverts the transaction if this condition is not met. If the caller is the contract owner, the function updates the _supportedTokens mapping to reflect the new support status for the specified token.

The executeProposal() is a function that allows the proposer of a proposal to execute it. The function first checks that the proposer is the one calling the function, that the proposal has not been executed yet, and that the number of "yes" votes is greater than the number of "no" votes. If all of these conditions are met, the function sets the executed flag to true, indicating that the proposal has been executed. Finally, any actions described in the proposal can be performed.

setMaxIdleTime function

Lastly, we have the setMaxIdleTime() function:

 function setMaxIdleTime(uint256 idleTime) external {
require(msg.sender == owner(), "Only owner can modify max idletime");
_maxIdleTime = idleTime;
}

This function allows the contract owner to set the maximum amount of time that can elapse without a user interacting with the contract before their tokens are considered "idle". The function takes in the idle time in seconds as an argument. The function first checks that the caller of the function is the contract owner using the owner function, and reverts the transaction if this condition is not met. If the caller is the contract owner, the function updates the _maxIdleTime variable to reflect the new maximum idle time.

Deployment

To deploy our smart contract successfully, we need the celo extention wallet which can be downloaded from here

Next, we need to fund our newly created wallet which can done using the celo alfojares faucet Here

You can now fund your wallet and deploy your contract using the celo plugin in remix.

Conclusion

That concludes our tutorial on the MultiTokenWallet contract! In summary, we've covered how to implement a simple multi-token wallet contract that allows users to deposit and withdraw various ERC20 tokens. The contract owner can add or remove support for different tokens, as well as set a maximum idle time.

Next Steps

I hope you learned a lot from this tutorial. Here are some relevant links that would aid your learning further.

About the author

I'm Jonathan Iheme, A full stack block-chain Developer from Nigeria.

Thank You!!

Go back

· 9 min read

header

Introduction

In this tutorial, we will be building a peer to peer lending and borrowing smart contract. This contract enables users to create, fund, and repay loans using the celo cUSD as a form of payment. It also defines the minimum and maximum loan amounts, as well as the minimum and maximum interest rates that can be set for a loan. By the end of the tutorial, you should have a good strong foundational knowledge on building a p2p lending and borrowing smart contract.

Project Repository

Requirements

Prerequisites

  • Solidity
  • Basic Understanding Of Blockchain Technology.

Writing The P2PLending Smart Contract

Now it's time to write your p2plending smart contract

This is how the completed code should look like.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract P2PLending {
// The minimum and maximum amount of ETH that can be loaned
uint public constant MIN_LOAN_AMOUNT = 0.1 ether;
uint public constant MAX_LOAN_AMOUNT = 10 ether;
// The minimum and maximum interest rate in percentage that can be set for a loan
uint public constant MIN_INTEREST_RATE = 1;
uint public constant MAX_INTEREST_RATE = 10;
struct Loan {
uint amount;
uint interest;
uint duration;
uint repaymentAmount;
uint fundingDeadline;
address borrower;
address payable lender;
bool active;
bool repaid;
}
mapping(uint => Loan) public loans;
uint public loanCount;
event LoanCreated(
uint loanId,
uint amount,
uint interest,
uint duration,
uint fundingDeadline,
address borrower,
address lender
);
event LoanFunded(uint loanId, address funder, uint amount);
event LoanRepaid(uint loanId, uint amount);
modifier onlyActiveLoan(uint _loanId) {
require(loans[_loanId].active, "Loan is not active");
_;
}
modifier onlyBorrower(uint _loanId) {
require(
msg.sender == loans[_loanId].borrower,
"Only the borrower can perform this action"
);
_;
}
function createLoan(
uint _amount,
uint _interest,
uint _duration
) external payable {
require(
_amount >= MIN_LOAN_AMOUNT && _amount <= MAX_LOAN_AMOUNT,
"Loan amount must be between MIN_LOAN_AMOUNT and MAX_LOAN_AMOUNT"
);
require(
_interest >= MIN_INTEREST_RATE && _interest <= MAX_INTEREST_RATE,
"Interest rate must be between MIN_INTEREST_RATE and MAX_INTEREST_RATE"
);
require(_duration > 0, "Loan duration must be greater than 0");
uint _repaymentAmount = _amount + (_amount * _interest) / 100;
uint _fundingDeadline = block.timestamp + (1 days);
uint loanId = loanCount++;
Loan storage loan = loans[loanId];
loan.amount = _amount;
loan.interest = _interest;
loan.duration = _duration;
loan.repaymentAmount = _repaymentAmount;
loan.fundingDeadline = _fundingDeadline;
loan.borrower = msg.sender;
loan.lender = payable(address(0));
loan.active = true;
loan.repaid = false;
emit LoanCreated(
loanId,
_amount,
_interest,
_duration,
_fundingDeadline,
msg.sender,
address(0)
);
}
function fundLoan(uint _loanId) external payable onlyActiveLoan(_loanId) {
Loan storage loan = loans[_loanId];
require(
msg.sender != loan.borrower,
"Borrower cannot fund their own loan"
);
require(loan.amount == msg.value, "not enough");
require(
block.timestamp <= loan.fundingDeadline,
"Loan funding deadline has passed"
);
payable(address(this)).transfer(msg.value);
loan.lender = payable(msg.sender);
loan.active = false;
emit LoanFunded(_loanId, msg.sender, msg.value);
}
function repayLoan(
uint _loanId
) external payable onlyActiveLoan(_loanId) onlyBorrower(_loanId) {
require(
msg.value == loans[_loanId].repaymentAmount,
"Incorrect repayment amount"
);
loans[_loanId].lender.transfer(msg.value);
loans[_loanId].repaid = true;
loans[_loanId].active = false;
emit LoanRepaid(_loanId, msg.value);
}
function getLoanInfo(
uint _loanId
)
external
view
returns (
uint amount,
uint interest,
uint duration,
uint repaymentAmount,
uint fundingDeadline,
address borrower,
address lender,
bool active,
bool repaid
)
{
Loan storage loan = loans[_loanId];
return (
loan.amount,
loan.interest,
loan.duration,
loan.repaymentAmount,
loan.fundingDeadline,
loan.borrower,
loan.lender,
loan.active,
loan.repaid
);
}
function withdrawFunds(uint _loanId) external onlyBorrower(_loanId) {
Loan storage loan = loans[_loanId];
require(loan.active = false);
payable(msg.sender).transfer(loan.amount);
}
}

Let's go over the code to see what's happening.

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

contract P2PLending {

// The minimum and maximum amount of ETH that can be loaned
uint constant public MIN_LOAN_AMOUNT = 0.1 ether;
uint constant public MAX_LOAN_AMOUNT = 10 ether;

// The minimum and maximum interest rate in percentage that can be set for a loan
uint constant public MIN_INTEREST_RATE = 1;
uint constant public MAX_INTEREST_RATE = 10;
}

First we declare the license and the solidity version we will be using. Then we define our constants for the minimum and maximum loan amounts, as well as the minimum and maximum interest rates that can be set for a loan. We did this by declaring the constants MIN_LOAN_AMOUNT, MAX_LOAN_AMOUNT, MIN_INTEREST_RATE, and MAX_INTEREST_RATE.

 struct Loan {
uint amount;
uint interest;
uint duration;
uint repaymentAmount;
uint fundingDeadline;
address borrower;
address payable lender;
bool active;
bool repaid;
}

The next step is to create a structure to store loan data. This structure, named Loan, includes the following fields: amount, interest, duration, repaymentAmount, fundingDeadline, borrower, lender, active, and repaid. Amount is the amount of ETH to be loaned. Interest is the interest rate in percentage for the loan. Duration is the amount of time the loan will last for. RepaymentAmount is the total amount to be repaid, which is calculated by adding the loan amount and the interest rate together. FundingDeadline is the deadline for the loan to be fully funded. Borrower is the address of the borrower. Lender is the address of the lender. Active is a boolean value, which is set to true if the loan is still active, and false if the loan has been funded or repaid. Repaid is a boolean value, which is set to true if the loan has been repaid, and false if the loan is still outstanding.

 mapping (uint => Loan) public loans;
uint public loanCount;

After the loan structure is defined, a mapping is created to store loan data. This mapping, named loans, stores Loan structs, and is indexed by uints. A uint, named loanCount, is also created to keep track of the number of loans that have been created.

 event LoanCreated(uint loanId, uint amount, uint interest, uint duration, uint fundingDeadline, address borrower, address lender);
event LoanFunded(uint loanId, address funder, uint amount);
event LoanRepaid(uint loanId, uint amount);

modifier onlyActiveLoan(uint _loanId) {
require(loans[_loanId].active, "Loan is not active");
_;
}

modifier onlyBorrower(uint _loanId) {
require(msg.sender == loans[_loanId].borrower, "Only the borrower can perform this action");
_;
}

We created Events to log loan activities, such as LoanCreated, LoanFunded and LoanRepaid.

We also added Modifiers to restrict certain functions to only the borrower, lender, and funders of the loan.

Now let's look at the functions.

 function createLoan(
uint _amount,
uint _interest,
uint _duration
) external payable {
require(
_amount >= MIN_LOAN_AMOUNT && _amount <= MAX_LOAN_AMOUNT,
"Loan amount must be between MIN_LOAN_AMOUNT and MAX_LOAN_AMOUNT"
);
require(
_interest >= MIN_INTEREST_RATE && _interest <= MAX_INTEREST_RATE,
"Interest rate must be between MIN_INTEREST_RATE and MAX_INTEREST_RATE"
);
require(_duration > 0, "Loan duration must be greater than 0");

uint _repaymentAmount = _amount + (_amount * _interest) / 100;
uint _fundingDeadline = block.timestamp + (1 days);

uint loanId = loanCount++;

Loan storage loan = loans[loanCount];

loan.amount = _amount;
loan.interest = _interest;
loan.duration = _duration;
loan.repaymentAmount = _repaymentAmount;
loan.fundingDeadline = _fundingDeadline;
loan.borrower = msg.sender;
loan.lender = payable(address(0));
loan.active = true;
loan.repaid = false;

emit LoanCreated(
loanId,
_amount,
_interest,
_duration,
_fundingDeadline,
msg.sender,
address(0)
);
}

The createLoan() function is used to create a loan. This function requires the loan amount, interest rate, and duration as parameters. It then calculates the repayment amount, which is the loan amount plus the interest rate, and sets the funding deadline to one day after the loan is created. It then creates a new loan, and stores it in the loans mapping, indexed by the loanCount uint. Finally, it emits a LoanCreated event.

  function fundLoan(uint _loanId) external payable onlyActiveLoan(_loanId) {
Loan storage loan = loans[_loanId];
require(
msg.sender != loan.borrower,
"Borrower cannot fund their own loan"
);
require(loan.amount == msg.value, "not enough");
require(
block.timestamp <= loan.fundingDeadline,
"Loan funding deadline has passed"
);
payable(address(this)).transfer(msg.value);
loan.lender = payable(msg.sender);
loan.active = false;

emit LoanFunded(_loanId, msg.sender, msg.value);
}

Next, is the fundLoan() function which is used to fund a loan. This function requires the loan ID as a parameter. It then checks if the loan is active, and if the loan is still needing to be funded. Then it checks that the sender of the transaction is not the borrower, and that the amount sent is not more than what is needed to fully fund the loan. it sets the loan to inactive. Finally, it emits a LoanFunded event.

function repayLoan(
uint _loanId
) external payable onlyActiveLoan(_loanId) onlyBorrower(_loanId) {
require(
msg.value == loans[_loanId].repaymentAmount,
"Incorrect repayment amount"
);

loans[_loanId].lender.transfer(msg.value);
loans[_loanId].repaid = true;
loans[_loanId].active = false;

emit LoanRepaid(_loanId, msg.value);
}

Next, the repayLoan() is then created, which is used to repay a loan. This function requires the loan ID as a parameter. It then checks if the loan is active, and if the sender is the borrower. It also checks that the amount sent is equal to the repayment amount of the loan. It then transfers the repayment amount to the lender, sets the loan to inactive, and sets the repaid boolean to true. Finally, it emits a LoanRepaid event.

function getLoanInfo(
uint _loanId
)
external
view
returns (
uint amount,
uint interest,
uint duration,
uint repaymentAmount,
uint fundingDeadline,
address borrower,
address lender,
bool active,
bool repaid
)
{
Loan storage loan = loans[_loanId];
return (
loan.amount,
loan.interest,
loan.duration,
loan.repaymentAmount,
loan.fundingDeadline,
loan.borrower,
loan.lender,
loan.active,
loan.repaid
);
}

Next, getLoanInfo() is then created, which is used to get information about a loan. This function requires the loan ID as a parameter. It then returns the loan amount, interest rate, duration, repayment amount, funding deadline, borrower address, lender address, active boolean, and repaid boolean.

function withdrawFunds(uint _loanId) external onlyBorrower(_loanId) {
Loan storage loan = loans[_loanId];
require(loan.active = false);

payable(msg.sender).transfer(loan.amount);
}

Finally, the withdrawFunds() function is created, which is used to withdraw funds from the contract. This function transfers the balance of the loan to the borrower.

Deployment

To deploy our smart contract successfully, we need the celo extention wallet which can be downloaded from here

Next, we need to fund our newly created wallet which can done using the celo alfojares faucet Here

You can now fund your wallet and deploy your contract using the celo plugin in remix.

Conclusions

In conclusion, This tutorial have covered how to create a smart contract for a P2P Lending contract. It has explained the process for creating a loan, funding a loan, repaying a loan, and getting loan information. It has also explained how the borrower can withdraw their funds from the contract.

Next Steps

I hope you learned a lot from this tutorial. Here are some relevant links that would aid your learning further.

About the author

I'm Jonathan Iheme, A full stack block-chain Developer from Nigeria.

Thank You!!

Go back

· 8 min read
Israel Okunaya

building-an-nft-marketplace-on-celo-with-python

Introduction

NFTs or Non-Fungible, an application of blockchain is used to represent unique digital assets such as collectibles, arts, etc. Celo is a blockchain platform that is designed to send, receive, and store digital assets securely on a mobile phone. The Celo blockchain is built using the Solidity programming language and is fully compatible with Ethereum. In this tutorial, we will be building an NFT marketplace with Python using the brownie framework and deploying it on Celo Testnet.

Prerequisites

To understand this tutorial, you must be familiar with the following:

  • Building Smart Contracts
  • The Python programming language

Requirements

You should have the following installed on your computer to follow along:

Setting Up Project

To get started, we have to create a new directory for our project and install the following dependencies:

mkdir nft-marketplace
cd nft-nft-marketplace
# Create virtual environment
python3.10 -m venv venv
# activate virtual environment
source venv/bin/activate

Note: Brownie works bet with python3.10

# Install ganache
npm install -g ganache
# Install eth-brownie and python-dotenv
pip3 install eth-brownie python-dotenv

After installing dependencies, we need to initialize our project as a brownie project.

brownie int

This command generates some folder which will look like this:

image

After initializing brownie into our project, in your root directory, create two files called .env and brownie-config.yaml. The .env file is used to store environment variables that should not be exposed to the public such as our private key, mnemonic phrase, etc, while brownie-config.yaml is used to configure brownie in our project.

.env

MNEMONIC="..."
PRIVATE_KEY="0x..."

brownie-config.yaml

reports:
exclude_contracts:
- SafeMath
depedencies:
- OpenZeppelin/openzeppelin-contracts@4.8.0
compiler:
solc:
version: 0.8.15
optimizer:
enabled: true
runs: 200
remappings:
- "@openzeppelin=OpenZeppelin/openzeppelin-contracts@4.8.0"
networks:
default: celo-alfajores
console:
show_colors: true
color_style: monokai
auto_suggest: true
completions: true
editing_mode: emacs
dotenv: .env
wallets:
from_mnemonic: ${MNEMONIC}
from_key: ${PRIVATE_KEY}

The next step is to add Celo Testnet (Alfajores) to our brownie project:

brownie networks add Celo celo-alfajores host=https://alfajores-forno.celo-testnet.org chainid=44787 explorer=https://alfajores-blockscout.celo-testnet.org

You can see the list of networs that have been added to our brownie project:

brownie network list

image

Implementing the Smart Contract

Next ,we have to write the smart contraxr for our NFT marketplace. In your contracts directory, create a new file called NFTMarketplace.sol

agma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
contract NFTMarketplace {
using SafeMath for uint256;
struct Auction {
address tokenAddress;
uint256 tokenId;
address payable seller;
uint256 price;
uint256 endTime;
bool active;
}
address public owner;
uint256 public feePercentage; // percentage of the sale price taken as fee
mapping(address => mapping(uint256 => Auction)) public auctions; // map of all active auctions
event AuctionCreated(
address indexed tokenAddress,
uint256 indexed tokenId,
address indexed seller,
uint256 price,
uint256 endTime
);
event AuctionEnded(
address indexed tokenAddress,
uint256 indexed tokenId,
address indexed buyer,
uint256 price
);
constructor() {
owner = msg.sender;
feePercentage = 1; // 1% fee by default
}
function createAuction(address _tokenAddress, uint256 _tokenId, uint256 _price, uint256 _duration) public {
require(_duration > 0, "Duration must be greater than zero");
require(_price > 0, "Price must be greater than zero");
IERC721 tokenContract = IERC721(_tokenAddress);
require(tokenContract.ownerOf(_tokenId) == msg.sender, "You don't own this NFT");
uint256 endTime = block.timestamp.add(_duration);
Auction memory auction = Auction(_tokenAddress, _tokenId, payable(msg.sender), _price, endTime, true);
auctions[_tokenAddress][_tokenId] = auction;
emit AuctionCreated(_tokenAddress, _tokenId, msg.sender, _price, endTime);
}
function endAuction(address _tokenAddress, uint256 _tokenId) public {
Auction storage auction = auctions[_tokenAddress][_tokenId];
require(auction.active, "Auction has already ended");
require(block.timestamp >= auction.endTime, "Auction hasn't ended yet");
address payable seller = auction.seller;
uint256 price = auction.price;
auction.active = false;
IERC721 tokenContract = IERC721(_tokenAddress);
tokenContract.safeTransferFrom(address(this), msg.sender, _tokenId);
uint256 fee = price.mul(feePercentage).div(100);
seller.transfer(price.sub(fee));
emit AuctionEnded(_tokenAddress, _tokenId, msg.sender, price);
}
function setFeePercentage(uint256 _feePercentage) public {
require(msg.sender == owner, "Only the owner can set the fee percentage");
require(_feePercentage >= 0 && _feePercentage <= 100, "Fee percentage must be between 0 and 100");
feePercentage = _feePercentage;
}
function withdraw() public {
require(msg.sender == owner, "Only the owner can withdraw funds");
address payable self = payable(address(this));
self.transfer(self.balance);
}
// fallback function
receive() external payable {}
}

Now let's go through the code step by step

pragma solidity ^0.8.0;
import "./IERC721.sol";
import "./IERC721Receiver.sol";
import "./SafeMath.sol";

The above code specifies our solidity version and imports the IERC721, IERC721Receiver, and SafeMath (for blockchain math operations) interfaces.

using SafeMath for uint256;
struct Auction {
address tokenAddress;
uint256 tokenId;
address payable seller;
uint256 price;
uint256 endTime;
bool active;
}

Enables us to use the SafeMath foe uint256 tyoe operations and the Auction struct defines the structure of the Auction.

mapping(address => mapping(uint256 => Auction)) public auctions;

This maps each token address and token ID to an active auction.

event AuctionCreated(
address indexed tokenAddress,
uint256 indexed tokenId,
address indexed seller,
uint256 price,
uint256 endTime
);
event AuctionEnded(
address indexed tokenAddress,
uint256 indexed tokenId,
address indexed buyer,
uint256 price
);

These are the eents that are emitted when an auction is creares and ended.

constructor() {
owner = msg.sender;
feePercentage = 1; // 1% fee by default
}

This sets the owner of the contract and the default fee (in percentage).

function createAuction(address _tokenAddress, uint256 _tokenId, uint256 _price, uint256 _duration) public {
require(_duration > 0, "Duration must be greater than zero");
require(_price > 0, "Price must be greater than zero");
IERC721 tokenContract = IERC721(_tokenAddress);
require(tokenContract.ownerOf(_tokenId) == msg.sender, "You don't own this NFT");
uint256 endTime = block.timestamp.add(_duration);
Auction memory auction = Auction(_tokenAddress, _tokenId, payable(msg.sender), _price, endTime, true);
auctions[_tokenAddress][_tokenId] = auction;
emit AuctionCreated(_tokenAddress, _tokenId, msg.sender, _price, endTime);
}
function endAuction(address _tokenAddress, uint256 _tokenId) public {
Auction storage auction = auctions[_tokenAddress][_tokenId];
require(auction.active, "Auction has already ended");
require(block.timestamp >= auction.endTime, "Auction hasn't ended yet");
address payable seller = auction.seller;
uint256 price = auction.price;
auction.active = false;
IERC721 tokenContract = IERC721(_tokenAddress);
tokenContract.safeTransferFrom(address(this), msg.sender, _tokenId);
uint256 fee = price.mul(feePercentage).div(100);
seller.transfer(price.sub(fee));
emit AuctionEnded(_tokenAddress, _tokenId, msg.sender, price);
}

The createAuction function creates a new auction and the endAuction ends an active auction.

function setFeePercentage(uint256 _feePercentage) public {
require(msg.sender == owner, "Only the owner can set the fee percentage");
require(_feePercentage >= 0 && _feePercentage <= 100, "Fee percentage must be between 0 and 100");
feePercentage = _feePercentage;
}
function withdraw() public {
require(msg.sender == owner, "Only the owner can withdraw funds");
address payable self = payable(address(this));
self.transfer(self.balance);
}

The setFeePercentage functino sets the percentage fee for the contract and the withdraw function enables the creator of the contract to withdraw the fund from it.

// fallback function
receive() external payable {}

A callback functnion that is called when ether is sent to the contract

Deploying the Contract

Next, we need to compile and deploy the contract on the Celo Testnet, Run the following command to compile the contract.

brownie compile

image

To deploy the smart contract on Celo create a new file called deploy.py in the scripts directory of your project.

from brownie import NFTMarketplace, accounts, config, network
def deploy_marketplace():
# Load the account to deploy from
dev = accounts.add(config["wallets"]["from_key"])
print(f"Deploying from {dev.address}")
# Deploy the contract
marketplace = NFTMarketplace.deploy({"from": dev})
print(f"NFTMarketplace contract deployed to {marketplace.address}")
def main():
# Set the network to deploy to
network_name = network.show_active()
print(f"Deploying to {network_name} network")
# Call the deploy function
deploy_marketplace()

The deploy_marketplace functino get the account we would use to deploy the contract.

Deploy the Contract

 Deploy the contract to the Celo Alfajores testnet
brownie run scripts/deploy.py --network celo-alfajores

image

Conclusion

In this tutorial we implemented an NFT smart contract that allows users to create and participate in ERC721 token auctions. The contract charges a fee for each sale, which is determined by the contract's owner. When an auction concludes, the winning bidder receives the NFT, while the seller receives the sale price less the fee. The contract also gives the owner the option to withdraw funds from the contract.

We have learned how to implement an NFT exchange on Celo using brownie in Python. We explored topics such as the basics of setting up a development environment, creating a smart contract and finally compiling and deploying the contract on Celo Testnet or Afrajores.

Next Step

We can explore more functionalities that we can add to this contract, such as the ability for bidders to increase the current bid by a minimum increment, the ability to see auction history, time extension on bid time, etc.

References

  1. Celo Developer Documentation
  2. Solidity Documentation
  3. Brownie Documentation
  4. Github Repository

About the Author

Israel Okunaya is an ace writer with a flair for simplifying complexities and a knack for storytelling. He leverages over four years of experience to meet the most demanding writing needs in different niches, especially food and travel, blockchain, and marketing. He sees blockchain as a fascinating yet tricky affair. So, he's given to simplifying its complexities with text and video tutorials.

Go back

· 9 min read

leveraging-blockchain-and-iot-for-refi-and-climate-actions-through-celo

Introduction​

There is no denying the individual potentials of blockchain and the Internet of Things (IoT)/hardware and the benefits of their intersection in facilitating ReFi and climate actions. What is missing is a conscious effort from developers, ReFi, and climate action advocates to leverage these tools (blockchain and IoT). There are only a few applications of IoT and blockchain for ReFi and climate action. This situation calls for more applications and products that leverage these tools. However, this will be possible with sound knowledge of focus areas, tools, and current use cases. Providing this knowledge is the focus of this tutorial.

Prerequisites​

Understanding the following concepts makes the knowledge in this tutorial a no-brainer:

Aside from the information in the embedded links, I will briefly explain each concept below.

Understanding the Basics

Before moving into these key elements of ReFi and areas where IoT/hardware can be applied, here are certain basic concepts you must first understand. These concepts are the basic knowledge needed to efficiently leverage blockchain and IoT tools for ReFi purposes.

  • Blockchain Technology

Blockchain is a distributed, immutable ledger that aids the process of recording transactions and keeping track of assets, which can be digital (cryptocurrencies), tangible (cash, landed properties, etc.), or non-tangible (copyrights, branding, etc.). Almost everything valuable can be tracked, kept safe, and traded using blockchain technology. Blockchain reduces security and ownership risks and cuts costs for every involved party.

  • Internet of Things

The Internet of Things (IoT) is a network of tangible objects (things) embedded with sensors, software, and other technologies. The purpose of this network is to connect and exchange data with other devices and systems over the internet for informational, resource provisioning, and validation purposes.

IoT devices vary from simple household items to sophisticated industrial tools. Today, there are more than 10 billion connected IoT devices. And experts expect this figure to rise to 22 billion by 2025.

  • Regenerative Finance (ReFi)

As coined by economist John Fullerton, “ReFi” institutes a process and financial system that uses markets to solve the issues markets create. Presently, blockchain technology and cryptocurrencies, as the most accessible market system yet, help bootstrap efforts to regenerate the world through a regenerative economy. And with platforms like the Celo blockchain, the future of ReFi and climate action is promising.

  • The Celo Ecosystem

Celo is a layer one blockchain protocol that promotes the ideals of ReFi “to build a financial system that creates the conditions for prosperity—for everyone.” The setup of the Celo ecosystem creates a platform for developers to leverage, enabling features to create a fresh burst of financial solutions accessible to mobile users with just a mobile number. This is possible on Celo thanks to the following key features:

  • Layer-1 protocol
  • EVM compatible
  • Proof-of-stake
  • Carbon negative
  • Mobile-first identity
  • Ultra-light clients
  • Localized stablecoins (cUSD, cEUR, cREAL)
  • Gas payable in multiple currencies

With blockchain technology, the Internet of Things, and Celo, the potentials are limitless in their applications towards creating a regenerative economy and facilitating climate action.

Potential Applications of Blockchain and IoT for ReFi and Climate Actions

The powers of blockchain technology and IoT in enabling practical climate actions and creating a regenerative economy are beyond what is presently obtainable. Below are other areas in blockchain and IoT that can be applied to critical elements of ReFi and facilitate climate actions.

1. Carbon Pricing

Carbon pricing is a famed mechanism with immense potential and numerous feats achieved over time. That explains how it continues to gain relevance and acceptance. This mechanism helps maximize market forces to tackle climate change by introducing financial incentives for companies and countries to reduce their emissions. Reductions can either be by embracing more environmental-friendly processes like using cleaner fuels.

Carbon pricing comes as either a carbon tax or charge or a cap-and-trade system (also known as the Emissions Trading System). The latter works based on government allotments or permits.

An ETS or a cap-trade system caps the total level of GHG emissions, allowing those industries with low emissions to trade their extra allowances for those that emit more. With supply and demand around emissions allowances, an ETS creates a market price for GHG emissions. Thanks to this cap/budget, you can ensure that the emission reductions happen to keep the emitters in check.

For carbon taxes, there is a direct price set on carbon emissions in the form of a defined tax rate on GHG emissions or, more commonly, on the carbon content of fossil fuels. Carbon taxing is different from an ETS in that the emission reduction outcome of a carbon tax is not predetermined, but the carbon price is.

The national and economic context determines the government's choice between these two measures.

Developers on Celo can leverage blockchain technology and the Internet of Things (IoT) for better carbon pricing with IoT tools like drones, GHG sensors, and oracles for data validation to determine the correct carbon prices. Also, these tools help gather validated data useful for blockchain tools like decentralized applications, which need this validated data to interact with smart contracts that implement carbon taxes or an emissions trading system.

2. Gamification for ReFi

Gamification is the application of gaming elements in non-gaming contexts to influence specific real-world behaviors. These gamified projects use gaming features like points, badges, leaderboards, and challenges to keep players’ attention. Then, they apply these gaming activities to real-world situations that are otherwise unable to increase engagement, like climate action. Through gamification, these ReFi products engage users, solve environmental problems like waste management and GHG emission control, and drive specific behaviors.

This initiative is becoming increasingly popular, both generally and in the ReFi space. Lately, gamification has increased attention to climate change and sustainability movements. An existing real-life gamification use case on Celo is IntotheVerse. This L2 platform is a Celo-based Metaverse project that utilizes a pixelated imitation of planet Earth. Game players live in a crypto-native city and have virtual banks and farms. Rewards on this gamified experience include tokens for eventual cash value and an avenue to contribute towards universal basic income (UBI) projects.

Other examples outside Celo include Wheelcoin, Littercoin, HumanDAO, Ecosapiens, Climate Guardians, etc. With IoT tools like drones and sensor nodes, these gamified platforms can effortlessly validate the carbon and resource data needed to estimate the impact of the gamified actions in real life.

3. Waste management and IoT

The possible impacts and potentials of IoT in waste management are exciting. IoT tools can be helpful data collectors, validators, metrics, and triggers to promote proper waste management, keep correct records and data and trigger incentives for proper waste management actions. IoT applications for waste management can include but are not limited to the following:

  • Smart waste bins for emission collection, proper solid waste collection, and sorting for recycling and upcycling purposes.
  • Sensor nodes for waste collection centers to determine the amount and type of waste collected at various locations.
  • Sensor nodes, drones, and geolocation devices for data collection and validation for Oracles of waste management platforms interested in collecting correct waste data.
  • Information provision and verification to trigger incentives on waste management platforms, incentivizing proper waste management actions.

The Wasset platform is an excellent example of an existing application of IoT in waste management on the Celo blockchain, even though it is still building. The Wasset whitepaper provides more details.

4. Mineral Conservation and Exploration

Here is another potential application of blockchain and IoT to create a regenerative economy and ensure sustainability.

Blockchain networks like Celo can help store mineral exploration data, deposit data, and other appropriate data updates around exploration and conservation efforts. Like other blockchains, this data record process works on the Celo blockchain in a decentralized manner.

Also, IoT sensor nodes can help mineral conservationists measure and validate mineral conservation concerns like water level, water consumption level, and mineral usage in specific areas. This process helps keep track of minerals and ensures problems get identified and taken care of early enough.

And when these issues are identified, blockchain smart contracts can ensure efforts to rectify them are profitable by leveraging information and validation from necessary IoT tools like sensors and oracles.

5. IoT for Risk and Natural Disaster Management

This possible application of blockchain and IoT on Celo is helpful for insurance companies, government parastatals, and organizations in the environmental and climate space. These bodies can effortlessly monitor environmental changes, indicating possible risks and natural disasters, by leveraging IoT sensor nodes and oracles.

These bodies can also leverage dapps on the Celo blockchain to interact with specific smart contracts designed to prompt actions and reactions to warning signs and reward actions directed at alleviating such risks. This application does not only protect our planet, but it also makes efforts to address these warnings rewarding through incentivization.

Conclusion

The Celo ecosystem allows and facilitates all of these applications and use cases at the intersection of blockchain and IoT for ReFi and climate action. With a few existing use cases like SavePlanetEarth, Toucan Protocol, etc., developers can leverage the Celo blockchain for new solutions in these aspects. Celo provides tools, community, and financial aid to ensure developers can quickly build and scale.

Next Steps

The Celo Docs will provide the resources needed to leverage these tools for ReFi and climate actions in the areas mentioned above and more as your next step.

About the Author

Boyejo Oluwafemi is a hardware product developer working at the intersection of hardware and blockchain technology. He’s working to leverage his wealth of experience working on several products ranging from smart health devices to sporting gadgets to deliver smart payment solutions for crypto for a more inclusive future.

References

Go back

· 10 min read
Ewerton Lopes

header

Prerequisite

  • The Remix IDE is an open-source web and desktop application for creating and deploying Smart Contracts. Originally created for Ethereum, it fosters a fast development cycle and has a rich set of plugins with intuitive GUIs. Remix is used for the entire journey of contract development and is a playground for learning and teaching EVM-compatible blockchains like Celo. Before starting this tutorial, see how Build Smart Contract on Celo with Remix

Introduction

The contract allows the buyer and seller to deposit funds into the contract. The contract then holds these funds until the end date specified in the contract. If the buyer or seller fulfills their obligations, they can withdraw the funds after the end date. If neither party fulfills their obligations, either the buyer or the seller can withdraw their deposit.

Code

Here are the steps to code this contract:

  1. Import the required packages This contract uses two external libraries: OpenZeppelin's ReentrancyGuard and SafeMath. These libraries provide additional functionality for our smart contract and prevent certain vulnerabilities that can occur while executing smart contracts.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";

  1. Define the contract and declare the contract variables

The contract has several variables that you need to declare. Here's a list of them and their types:

  • seller: address payable
  • buyer: address payable
  • feeAddress: address payable
  • contractBalance: uint256
  • sellerBalance: uint256
  • buyerBalance: uint256
  • securityDeposit: uint256
  • sellerPreAgreementAmount: uint256
  • buyerPreAgreementAmount: uint256
  • endDate: uint256
  • daysForWithdrawal: uint256
  • transferFee: uint256
  • fee: uint256

Here's how to declare them:

contract Escrow is ReentrancyGuard {
address payable seller;
address payable buyer;
address payable feeAddress;

uint256 public contractBalance;
uint256 public sellerBalance;
uint256 public buyerBalance;

uint256 public securityDeposit;
uint256 public sellerPreAgreementAmount;
uint256 public buyerPreAgreementAmount;
uint256 public endDate;
uint256 public daysForWithdrawal;

uint256 transferFee;
uint256 fee;
}
  1. Event when funds are deposited
event FundsDeposited(address indexed depositor, uint256 amount);

event Deposit(address indexed contractAddress, address indexed from, uint amount);
  1. Declare the SafeMath The code using SafeMath for uint256; is importing the SafeMath library and applying it to the uint256 data type.

In Solidity, the uint256 data type is an unsigned integer that has a range of 0 to 2^256-1. This data type is commonly used for storing and manipulating large numbers, such as those involved in cryptocurrency transactions.

The SafeMath library is a set of functions that provides arithmetic operations for uint256 numbers with added protection against integer overflow and underflow. Integer overflow and underflow are common errors in Solidity where the result of an arithmetic operation exceeds the maximum or minimum value that can be stored in a data type. These errors can lead to unexpected results or even security vulnerabilities in smart contracts.

By using the SafeMath library, developers can avoid these errors and ensure that arithmetic operations involving uint256 numbers are performed safely.

    using SafeMath for uint256;
  1. Create the constructor Create the constructor that will initialize the contract with the required parameters: addresses of the seller and buyer, the security deposit, pre-agreement amounts, end date, days for withdrawal, and the fee address
constructor(
address payable _seller,
address payable _buyer,
uint256 _securityDeposit,
uint256 _sellerPreAgreementAmount,
uint256 _buyerPreAgreementAmount,
uint256 _endDate,
uint256 _daysForWithdrawal,
address _feeAddress
) {
seller = _seller;
buyer = _buyer;
securityDeposit = _securityDeposit;
sellerPreAgreementAmount = _sellerPreAgreementAmount;
buyerPreAgreementAmount = _buyerPreAgreementAmount;
endDate = _endDate;
daysForWithdrawal = _daysForWithdrawal;
transferFee = 1;
feeAddress = payable(_feeAddress);
}
  1. Create the fallback and receive functions The fallback and receive functions allow the contract to accept funds. Here's how to create them:
fallback() external payable {
emit Deposit(address(this), msg.sender, msg.value);
}

receive() external payable {
emit Deposit(address(this), msg.sender, msg.value);
}
  1. Create the deposit function The deposit function allows either the seller or buyer to deposit funds into the contract. Here's how to create it:
function deposit(address _user) external payable {
require(_user == seller || _user == buyer, "Only the seller or buyer can deposit funds");
if (_user == seller) {
sellerBalance += msg.value;
} else {
buyerBalance += msg.value;
}
contractBalance += msg.value;
emit FundsDeposited(_user, msg.value);
}
  1. Withdraw Function for the buyer or seller to withdraw the money.
function withdraw(address _user) external payable nonReentrant {
require (address(this).balance != 0, "Contract don't have funds");
require (block.timestamp < endDate, "Time is not over yet");
fee = address(this).balance.mul(transferFee).div(100);
// If the end date has passed but don't have passed enddate + daystowithdraw and the seller have fulfilled his or her obligations, the seller can withdraw the funds
if (sellerBalance > sellerPreAgreementAmount && block.timestamp > endDate && block.timestamp < endDate + (daysForWithdrawal * 1 days)) {
require (seller == _user, "You are not the seller");
seller.transfer(address(this).balance.sub(fee));
feeAddress.transfer(fee);
}
// The buyer didn't withdraw the balance. So he or she backed out of the deal
else if (buyerBalance > securityDeposit && block.timestamp > endDate + (daysForWithdrawal * 1 days)) {
require (buyer == _user, "You are not the buyer");
buyer.transfer(address(this).balance.sub(fee));
feeAddress.transfer(fee);
}
// The time expired but neither of the two deposited the pre-agreement
else if (address(this).balance != 0 && sellerBalance < sellerPreAgreementAmount && buyerBalance < buyerPreAgreementAmount && block.timestamp > endDate + (daysForWithdrawal * 1 days)){
require(_user == buyer || _user == seller, "Just buyer or seller can withdraw");
if (_user == buyer && buyerBalance != 0){
fee = buyerBalance.mul(transferFee).div(100);
buyer.transfer(buyerBalance.sub(fee));
feeAddress.transfer(fee);
buyerBalance = 0;
}
else if(_user == seller && sellerBalance != 0) {
fee = sellerBalance.mul(transferFee).div(100);
seller.transfer(sellerBalance.sub(fee));
feeAddress.transfer(fee);
sellerBalance = 0;
}
}

}

  1. Return functions

This smart contract contains a series of functions that allow other contracts or external parties to access specific data stored in the contract. Each function is defined with the public keyword, which means that they can be accessed by anyone on the network. The view keyword indicates that these functions do not modify any data within the contract and simply return data.

Here's a brief explanation of each function:

  • returnSeller() - This function returns the address of the seller in the contract.

    function returnSeller() public view returns(address){
    return seller;
    }
  • returnBuyer() - This function returns the address of the buyer in the contract.

    function returnBuyer() public view returns(address){
    return buyer;
    }
  • returnSellerBalance() - This function returns the current balance of the seller in the contract.

    function returnSellerBalance() public view returns(uint){
    return sellerBalance;
    }
  • returnBuyerBalance() - This function returns the current balance of the buyer in the contract.

    function returnBuyerBalance() public view returns(uint){
    return buyerBalance;
    }
  • returnSecurityDeposit() - This function returns the security deposit amount stored in the contract.

    function returnSecurityDeposit() public view returns(uint){
    return securityDeposit;
    }
  • returnSellerPreAgreementAmount() - This function returns the amount that the seller had agreed to before the contract was created.

    function returnSellerPreAgreementAmount() public view returns(uint){
    return sellerPreAgreementAmount;
    }
  • returnBuyerPreAgreementAmount() - This function returns the amount that the buyer had agreed to before the contract was created.

    function returnBuyerPreAgreementAmount() public view returns(uint){
    return buyerPreAgreementAmount;
    }
  • returnEndDate() - This function returns the end date of the contract.

    function returnEndDate() public view returns(uint){
    return endDate;
    }
  • returnDaysForWithdrawal() - This function returns the number of days allowed for withdrawal after the contract is created.

    function returnDaysForWithdrawal() public view returns(uint){
    return daysForWithdrawal;
    }

Full Code

You can see the full code below or access it on github

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";

//contract Escrow
contract escrow is ReentrancyGuard {
// Variables to store the addresses of the seller and buyer
address payable seller;
address payable buyer;
address payable feeAddress;

// Variable to store the balance of the contract
uint256 public contractBalance;
uint256 public sellerBalance;
uint256 public buyerBalance;

// Variables to store the security deposit, pre-agreement amounts, end date, and days for withdrawal
uint256 public securityDeposit;
uint256 public sellerPreAgreementAmount;
uint256 public buyerPreAgreementAmount;
uint256 public endDate;
uint256 public daysForWithdrawal;

//Application fee
uint transferFee;
uint fee;

// Event to emit when funds are deposited
event FundsDeposited(address indexed depositor, uint256 amount);

// Deposit event
event Deposit(address indexed contractAddress, address indexed from, uint amount);

using SafeMath for uint256;

// Constructor to initialize the contract with the addresses of the seller and buyer, the security deposit, pre-agreement amounts, end date, and days for withdrawal
constructor(address payable _seller, address payable _buyer, uint256 _securityDeposit, uint256 _sellerPreAgreementAmount, uint256 _buyerPreAgreementAmount, uint256 _endDate, uint256 _daysForWithdrawal, address _feeaddress) {
seller = _seller;
buyer = _buyer;
securityDeposit = _securityDeposit;
sellerPreAgreementAmount = _sellerPreAgreementAmount;
buyerPreAgreementAmount = _buyerPreAgreementAmount;
endDate = _endDate;
daysForWithdrawal = _daysForWithdrawal;
transferFee = 1;
feeAddress = payable(_feeaddress);
}

//Fallback function
fallback () external payable {
emit Deposit(address(this), msg.sender, msg.value);
}

//Receive function
receive () external payable {
emit Deposit(address(this), msg.sender, msg.value);
}


// Function to deposit funds to the contract
function deposit(address _user) external payable {
require(_user == seller || _user == buyer, "Only the seller or buyer can deposit funds");
if (_user == seller) {
sellerBalance += msg.value;
} else {
buyerBalance += msg.value;
}
contractBalance += msg.value;
emit FundsDeposited(_user, msg.value);
}



// Function for the buyer or seller to withdraw the security deposit

function withdraw(address _user) external payable nonReentrant {
require (address(this).balance != 0, "Contract don't have funds");
require (block.timestamp < endDate, "Time is not over yet");
fee = address(this).balance.mul(transferFee).div(100);
// If the end date has passed but don't have passed enddate + daystowithdraw and the seller have fulfilled his or her obligations, the seller can withdraw the funds
if (sellerBalance > sellerPreAgreementAmount && block.timestamp > endDate && block.timestamp < endDate + (daysForWithdrawal * 1 days)) {
require (seller == _user, "You are not the seller");
seller.transfer(address(this).balance.sub(fee));
feeAddress.transfer(fee);
}
// The buyer didn't withdraw the balance. So he or she backed out of the deal
else if (buyerBalance > securityDeposit && block.timestamp > endDate + (daysForWithdrawal * 1 days)) {
require (buyer == _user, "You are not the buyer");
buyer.transfer(address(this).balance.sub(fee));
feeAddress.transfer(fee);
}
// The time expired but neither of the two deposited the pre-agreement
else if (address(this).balance != 0 && sellerBalance < sellerPreAgreementAmount && buyerBalance < buyerPreAgreementAmount && block.timestamp > endDate + (daysForWithdrawal * 1 days)){
require(_user == buyer || _user == seller, "Just buyer or seller can withdraw");
if (_user == buyer && buyerBalance != 0){
fee = buyerBalance.mul(transferFee).div(100);
buyer.transfer(buyerBalance.sub(fee));
feeAddress.transfer(fee);
buyerBalance = 0;
}
else if(_user == seller && sellerBalance != 0) {
fee = sellerBalance.mul(transferFee).div(100);
seller.transfer(sellerBalance.sub(fee));
feeAddress.transfer(fee);
sellerBalance = 0;
}
}

}


function returnSeller() public view returns(address){
return seller;
}

function returnBuyer() public view returns(address){
return buyer;
}

function returnSellerBalance() public view returns(uint){
return sellerBalance;
}

function returnBuyerBalance() public view returns(uint){
return buyerBalance;
}


function returnSecurityDeposit() public view returns(uint){
return securityDeposit;
}

function returnSellerPreAgreementAmount() public view returns(uint){
return sellerPreAgreementAmount;
}

function returnBuyerPreAgreementAmount() public view returns(uint){
return buyerPreAgreementAmount;
}

function returnEndDate() public view returns(uint){
return endDate;
}

function returnDaysForWithdrawal() public view returns(uint){
return daysForWithdrawal;
}

}

Next step

As the next steps, I suggest you consult other Celo tutorials using Remix here.

About the author

I am a serial entrepreneur, founder of Guizo Studios, and always available to help the Celo ecosystem.

LinkedIn

Go back

· 21 min read
Ewerton Lopes

header

Introduction

Smart contracts are self-executing contracts with the terms of the agreement between buyer and seller being directly written into lines of code. They can be used to automate and enforce the negotiation or performance of a contract. However, since smart contracts operate on a decentralized blockchain network, they are often subject to attacks and vulnerabilities that can compromise the security and functionality of the contract.

To ensure the security and functionality of your smart contract, it is important to prepare for auditing your code. Auditing is the process of reviewing your smart contract's code for errors, vulnerabilities, and potential weaknesses. The goal is to identify and fix any issues before the contract is deployed on the blockchain network.

Here are some reasons why you should prepare for auditing your smart contract:

Ensure Security: Auditing can help identify potential security risks and vulnerabilities in your smart contract, preventing hackers from exploiting any weaknesses and stealing your assets.

Improve Functionality: Auditing can help improve the functionality of your smart contract by identifying potential bugs or errors in your code, which can prevent your contract from executing as intended.

Increase User Trust: Auditing can help increase user trust in your smart contract, as users will be more likely to use and interact with a contract that has undergone a thorough auditing process.

Accuracy: The code in smart contracts is executed automatically without human intervention, making accuracy crucial. Auditing helps ensure that the code is accurate and functions as intended.

Reputation: A smart contract with a security breach or coding error can damage the reputation of the project or organization. Auditing helps prevent such incidents and enhances the reputation of the project.

Compliance: In some cases, smart contracts may need to comply with legal or regulatory requirements. Auditing helps ensure compliance and prevent any legal or regulatory violations.

Issue Levels – Audit Report

During a smart contract audit, the auditor reviews the code and conducts testing to identify any potential security risks or vulnerabilities. The auditor provides a report summarizing their findings and recommendations for addressing any issues. The report typically includes a description of the issues identified and their severity level. Here are the levels of issues that may be found in a smart contract audit report:

Critical: A critical issue is a severe vulnerability that could result in the loss or theft of assets. This includes issues such as the ability of an attacker to execute arbitrary code or steal user funds. These issues require immediate attention and remediation.

High: A high issue is a serious vulnerability that could lead to the compromise of the contract's security. This includes issues such as denial-of-service attacks, which could render the contract unusable. These issues also require immediate attention and remediation.

Medium: A medium issue is a moderate vulnerability that could impact the performance or functionality of the contract. This includes issues such as code inefficiencies, which could result in increased gas costs for users. These issues should be addressed as soon as possible.

Low: A low issue is a minor vulnerability that may not have a significant impact on the contract's functionality or security. This includes issues such as naming conventions or documentation errors. These issues should be addressed at the discretion of the contract owner.

Informational: An informational issue is not a security vulnerability but provides additional information that may be useful to the contract owner. This includes issues such as coding best practices or suggestions for improving the user experience.

It is important to note that the severity level of an issue depends on the specific context of the contract and its intended use. What may be a critical issue for one contract may be a low issue for another. The auditor will work with the contract owner to determine the appropriate level of severity for each issue and provide recommendations for remediation. In summary, a smart contract audit report will provide a detailed analysis of the issues identified, their severity level, and recommendations for addressing them.

Common audit process and methodology

The smart contract audit process and methodology typically involves the following steps:

Pre-Audit Planning: This involves understanding the contract's purpose, scope, and expected outcomes. The auditor reviews the contract's documentation, requirements, and specifications to determine the audit's objectives.

Code Review: The auditor conducts a thorough review of the contract's code to identify potential security vulnerabilities and performance issues. The auditor may use manual or automated tools to analyze the code, identify patterns, and assess the quality of the code.

Testing: The auditor conducts a series of tests to evaluate the contract's functionality and performance. These tests may include functional testing, regression testing, stress testing, and security testing.

Reporting: The auditor prepares a report detailing the findings of the audit. The report includes a summary of the issues identified, their severity level, and recommendations for addressing them. The report may also include a detailed analysis of the contract's performance, usability, and compliance with industry standards and best practices.

Remediation: The contract owner works to address the issues identified in the audit report. This may involve making changes to the code, updating documentation, or implementing new security measures. The auditor may provide guidance and support during the remediation process.

Follow-up Audit: The auditor may conduct a follow-up audit to verify that the issues identified in the initial audit report have been addressed. This is important to ensure that the contract remains secure and functional over time.

The methodology for conducting a smart contract audit may vary depending on the auditor's approach, the complexity of the contract, and the desired outcomes of the audit. However, a thorough audit process typically involves a combination of code review, testing, and reporting to identify and mitigate potential security risks and vulnerabilities in the contract. By following a rigorous audit process, contract owners can ensure that their contracts are secure, accurate, and compliant with industry standards and best practices.

Prepare for auditing your smart contract

Here are some steps to prepare for auditing your smart contract:

Choose a Reputable Auditor: Choose a reputable auditing firm or individual to review your smart contract's code. Look for auditors with experience in auditing smart contracts and a track record of providing high-quality audits.

Use a Solidity Linter: A Solidity linter is a tool that checks your code for potential issues and errors. It is a good idea to use a linter before submitting your code for auditing.

Follow Best Practices: Follow best practices when coding your smart contract, such as using secure coding techniques, avoiding hardcoded values, and thoroughly testing your code.

Libraries like No-Reentrant, pause/unpause: using libraries and functions that have been developed specifically to address common security issues in smart contracts. Here are two examples:

  • No-Reentrant Libraries: No-Reentrant libraries are designed to prevent reentrancy attacks, which can allow an attacker to repeatedly call a contract function while it is still executing. This can lead to unintended side effects and potentially result in the theft of funds. No-Reentrant libraries provide a way to lock a contract function so that it cannot be called again until it has finished executing. This helps to prevent reentrancy attacks and improve the security of the contract.
  • Pause/Unpause Functions: Pause/unpause functions are designed to allow contract owners to pause the execution of the contract in the event of an emergency or security issue. These functions provide a way to temporarily suspend the contract's operations, preventing any further transactions from occurring until the issue has been resolved. This can help to prevent the loss of funds and protect the contract's users from potential security risks.

Merge functions: Merging functions involves consolidating similar functions to reduce the overall size and complexity of the contract. Here are some tips on how to prepare for a smart contract audit report by merging functions:

  • Identify Similar Functions: Review the contract's code and identify functions that perform similar tasks or have similar logic. This may involve analyzing the code's structure and identifying patterns.

  • Consolidate Functions: Merge similar functions into a single function to reduce the overall size and complexity of the contract. This can improve readability, reduce the risk of errors, and simplify the contract's logic.

  • Ensure Consistency: Ensure that the merged function is consistent with the contract's documentation, requirements, and specifications. This can help ensure that the contract functions as intended and is compliant with industry standards and best practices.

  • Test the Merged Function: Test the merged function to ensure that it performs as expected and does not introduce any new vulnerabilities or issues. This may involve conducting functional testing, regression testing, and security testing.

  • Document the Changes: Document the changes made to the contract, including the functions that were merged and the rationale behind the decision. This can help ensure that future developers and auditors understand the contract's logic and can easily identify any potential issues or vulnerabilities.

Gas Optimisation: is a crucial aspect of smart contract development that can greatly impact the efficiency, cost, and security of the contract. Here are some tips on how to prepare for a smart contract audit report by optimizing gas usage:

  • Minimize Storage Usage: Every piece of data that is stored on the blockchain incurs a gas cost. Therefore, minimizing the amount of storage used by the contract can significantly reduce gas costs. This can be achieved by using data structures that are optimized for gas usage, such as arrays, bytes, and structs.

  • Reduce Loops and Iterations: Loops and iterations are computationally expensive operations that can consume a lot of gas. Therefore, minimizing the number of loops and iterations in the contract can reduce gas costs. This can be achieved by using optimized algorithms and data structures.

  • Use Optimized Solidity Code: Solidity is the programming language used to write smart contracts on the Celo blockchain. Using optimized Solidity code can significantly reduce gas costs. This can be achieved by following best practices and using optimized coding patterns.

  • Use External Libraries: External libraries can be used to reduce the amount of code that needs to be executed on the blockchain. This can significantly reduce gas costs. However, it is important to ensure that the external libraries used are secure and do not introduce new security risks.

  • Test Gas Usage: Gas usage can be tested using tools such as Ganache or Remix. These tools simulate the execution of the contract and provide detailed information on the gas usage of each operation. This can be used to identify gas-intensive operations and optimize them.

Withdrawal functions: are an essential part of many smart contracts, allowing users to withdraw funds or assets from the contract. However, these functions can also be a potential source of security vulnerabilities if not implemented properly. Here are some steps to be prepared for a smart contract audit report with regards to withdrawal functions:

  • Use Standard Libraries: It is recommended to use standard libraries for withdrawal functions to ensure that they are secure and well-tested. This helps to reduce the risk of vulnerabilities and errors in the code.

  • Limit Access: Limit the access to withdrawal functions to authorized users only. This can be done by implementing role-based access control or requiring authentication before allowing withdrawals.

  • Implement Security Measures: Implement security measures such as two-factor authentication, multi-signature verification, and time-delayed withdrawals to prevent unauthorized withdrawals and mitigate the risk of potential attacks.

  • Test Withdrawal Functions: Test the withdrawal functions extensively to identify any potential issues or vulnerabilities. This includes functional testing, regression testing, stress testing, and security testing.

Relevant checks: It is important for smart contract developers to place relevant checks first in their code because it helps to improve the overall security and functionality of the contract. Relevant checks refer to security and validation checks that are necessary to ensure that the contract is functioning as intended and that user inputs are validated and processed correctly.

By placing relevant checks first in the code, developers can ensure that critical security and validation checks are performed before other parts of the code are executed. This helps to prevent potential security vulnerabilities and reduce the risk of errors or bugs in the contract. For example, if a contract processes a user input before validating it, this could result in unexpected behavior or security vulnerabilities.

In addition, placing relevant checks first in the code helps to improve the overall performance of the contract. By validating user inputs and performing necessary security checks early in the code execution, the contract can avoid unnecessary computations and improve the efficiency of the code.

Naming conventions: are an important aspect of smart contract development and auditing. They help to ensure consistency and clarity in the code, making it easier for auditors and other developers to understand and review the contract. Here are some tips on how to prepare for auditing your smart contract by following naming conventions:

  • Use descriptive and meaningful names: When naming variables, functions, and other components of the contract, use descriptive and meaningful names that accurately reflect their purpose and functionality. This makes it easier for auditors to understand the code and identify potential issues.
  • Follow a consistent naming convention: Use a consistent naming convention throughout the code to make it easier to read and understand. This includes using consistent capitalization, avoiding special characters, and using underscores to separate words.
  • Avoid abbreviations and acronyms: Avoid using abbreviations or acronyms in the code unless they are commonly understood or widely accepted in the industry. Using abbreviations can make the code harder to understand and may lead to confusion or errors.
  • Use comments to explain complex code: Use comments to explain complex code or functions that may be difficult to understand. This helps auditors to quickly identify the purpose and functionality of different parts of the code.
  • Use clear and concise language: Use clear and concise language when naming components of the contract and writing comments. This helps to reduce the risk of misunderstandings and ensures that the code is easily understandable.

Contract Order: is an important consideration in smart contract development. It refers to the sequence in which different parts of the contract are executed during runtime. The order of contract execution can have a significant impact on the contract's functionality, performance, and security.

Here are some factors to consider when determining the contract order:

  • Security: Security should be the primary consideration when determining the contract order. The most critical security checks and validations should be executed first to ensure that user inputs are processed securely and to prevent potential vulnerabilities such as reentrancy attacks.
  • Dependencies: Smart contracts often have dependencies on other contracts or external systems such as oracles. In such cases, it is important to ensure that dependencies are executed in the correct order to ensure that the contract functions as intended.
  • Gas Efficiency: Gas efficiency is a critical consideration for smart contract developers as every transaction on the blockchain requires a certain amount of gas to execute. Contract order can impact gas efficiency, so it is important to structure the code in a way that minimizes gas usage.
  • Performance: Contract order can also impact performance, so developers should consider the most efficient order of execution to ensure that the contract functions optimally.

Missing functionality: in smart contract development refers to situations where the code does not include all of the necessary features or functions to meet the intended use case. This can occur due to a variety of reasons, including miscommunication between stakeholders, lack of planning, or inadequate testing.

Missing functionality can have significant consequences for the smart contract, including security vulnerabilities, incorrect behavior, or failed transactions. For example, if a contract is intended to process a certain type of input but is missing the necessary validation checks, this could result in unexpected behavior or security vulnerabilities.

To avoid missing functionality in smart contract development, it is important to take a structured approach to the development process. This includes proper planning, stakeholder communication, and thorough testing. During the planning phase, it is important to identify all of the necessary features and functions and ensure that they are included in the requirements and specifications.

Data validation: is an important aspect of smart contract development that involves checking and verifying user inputs to ensure that they are valid and meet certain criteria. Proper data validation helps to prevent errors, vulnerabilities, and other issues that can compromise the security and functionality of the contract.

Here are some key considerations for data validation in smart contract development:

  • Validate all inputs: All user inputs should be validated to ensure that they are of the correct type, within the expected range, and meet any other relevant criteria. This includes inputs such as addresses, amounts, and other parameters.
  • Use appropriate data types: The correct data type should be used for each input parameter to ensure that it can be correctly processed and validated. This includes using integer types for whole numbers, string types for text inputs, and so on.
  • Sanitize user inputs: User inputs should be sanitized to remove any potentially harmful or malicious characters or code. This can help to prevent security vulnerabilities such as SQL injection or cross-site scripting attacks.
  • Implement error handling: Proper error handling should be implemented to detect and handle invalid inputs. This can include returning error messages or rejecting invalid inputs altogether.
  • Use testing and debugging tools: Testing and debugging tools can be used to identify and resolve data validation issues during development. These tools can help to catch errors and vulnerabilities early in the development process, before they become larger issues.

Event emission on critical functions: is an important aspect of smart contract development. Events are a way for contracts to communicate with external systems and notify them when important actions or changes occur within the contract. Emitting events on critical functions can provide valuable information to users and external systems about the status and actions of the contract.

When a critical function is executed in a smart contract, such as a transfer of tokens or an update to the contract state, emitting an event can provide transparency and accountability for the action. By emitting an event, the contract can notify external systems and users of the action and provide relevant details such as the sender, receiver, amount, and timestamp of the action. This can help to prevent fraud or errors and improve trust in the contract.

In addition, emitting events on critical functions can provide valuable insights and analytics for contract owners and users. By analyzing the events emitted by the contract, users can gain insights into the usage and behavior of the contract and identify any potential issues or areas for improvement.

It is important to note that emitting events on critical functions should be done thoughtfully and with consideration for the privacy and security of users. The events emitted should provide relevant information without exposing sensitive information or compromising the security of the contract.

Access control: is a critical aspect of smart contract development as it helps to ensure that only authorized users can interact with the contract and perform specific actions. Modifiers are a feature in Solidity, the programming language used for Celo smart contract development, that can be used to enforce access control.

Modifiers are a type of function that can be added to a smart contract to restrict access to specific functions or actions. They are typically used to ensure that only authorized users can perform certain actions, such as transferring funds or updating contract state. Modifiers can be used in combination with other programming features, such as event logging and error handling, to create a comprehensive access control system for a smart contract.

Contract configuration: is an important aspect of smart contract development that involves setting various parameters and values that affect the behavior and functionality of the contract. These configuration settings are typically defined in the contract code and can be adjusted by the contract owner or administrator as needed.

Some examples of configuration settings that may be included in a smart contract include:

  • Gas Limits: Gas limits determine the maximum amount of gas that can be used to execute a transaction or function call in the contract. Setting appropriate gas limits is important to ensure that the contract can be executed efficiently and without running out of gas.
  • Security Settings: Security settings may include parameters such as access controls, permissions, and other security measures to prevent unauthorized access or manipulation of the contract.
  • Contract Parameters: Contract parameters may include values such as contract expiration date, contract owner, or other parameters that define the behavior of the contract.
  • External Contracts and Addresses: Smart contracts may interact with external contracts or addresses. Configuring these external interactions correctly is important to ensure that the contract can interact with external contracts securely and accurately.
  • Token Parameters: If the contract includes tokens, various token parameters such as total supply, decimal places, and symbol may need to be configured.

Unrestricted action or call: An unrestricted action or call in smart contract development refers to a scenario where a user or external entity is able to execute a contract function or modify contract state without proper authorization or validation. This can result in security vulnerabilities, data loss, or financial loss for the contract owner and users.

Unrestricted actions or calls can occur in a variety of ways, such as:

  • Lack of Access Control: If a contract function does not properly enforce access control, it may be possible for an unauthorized user to execute the function and modify the contract state.
  • Input Validation: If a contract function does not properly validate user input, it may be possible for an attacker to submit malicious input that can exploit vulnerabilities in the contract.
  • Overflows and Underflows: If a contract function uses variables that are not properly bounded or checked for overflow and underflow conditions, it may be possible for an attacker to modify contract state or execute unauthorized actions.

To prevent unrestricted actions or calls in smart contract development, developers should follow best practices such as:

  • Implement Access Control: Contracts should implement access control to ensure that only authorized users can execute contract functions and modify contract state.
  • Validate User Input: Contracts should validate user input to ensure that it is properly formatted, within expected limits, and does not contain malicious code or commands.
  • Use Safe Math Operations: Contracts should use safe math operations to prevent integer overflow and underflow vulnerabilities.

Post-Auditing Best Practices for Secure and Trustworthy Smart Contracts

Once a smart contract has been audited, it is important to follow post-auditing best practices to ensure its security, functionality, and user trust. Here are some best practices to consider:

Review the Auditor's Report: Once the auditor has completed their review, carefully review their report and address any issues or recommendations they have made. This can help you identify potential vulnerabilities and ensure that your smart contract is secure and functional.

Address Any Issues or Recommendations: If the auditor has identified any issues or made recommendations, it is important to address them as soon as possible. This can help you avoid potential security vulnerabilities and ensure that your smart contract functions as intended.

Thoroughly Test the Contract: After making any necessary changes based on the auditor's report, thoroughly test your smart contract to ensure it functions as intended. This can help you identify any remaining issues or bugs and ensure that your smart contract is secure and functional.

Implement Continuous Monitoring: Once your smart contract is live, it is important to implement continuous monitoring to identify any potential security threats or issues. This can help you address issues as soon as they arise and ensure the ongoing security and functionality of your smart contract.

Keep Your Contract Up to Date: As technology and security threats evolve, it is important to keep your smart contract up-to-date. This may involve making updates to the contract code or implementing new security measures to ensure its ongoing security and functionality.

By following these post-auditing best practices, you can ensure the security, functionality, and user trust of your smart contract. It is important to take a proactive approach to smart contract development and maintenance to ensure that your contract is secure and trustworthy for its users.

Conclusion

In conclusion, auditing is a crucial step in the development of a smart contract as it helps to identify potential vulnerabilities and security threats. By conducting a smart contract audit and following best practices, developers can ensure that their contracts are secure, functional, and trustworthy for their users.

As the world of smart contracts and decentralized finance continues to grow, it is important for developers to take a proactive approach to smart contract development and maintenance to ensure the safety and security of their users.

Next step

As the next steps, I suggest you consult other Celo tutorials here.

About the author

I am a serial entrepreneur, founder of Guizo Studios, and always available to help the Celo ecosystem.

LinkedIn