Saltar al contenido principal

96 publicaciones etiquetados con "solidity"

Ver Todas las Etiquetas
Go back

· 16 min de lectura
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 de lectura

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 de lectura

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 de lectura

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 de lectura
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

· 10 min de lectura
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 de lectura
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

Go back

· 8 min de lectura
Kunal Dawar

header

🌱 Introduction

In recent years, Non-Fungible Tokens (NFTs) have gained immense popularity in the world of art, music, and gaming. NFTs are unique digital assets that use blockchain technology to provide proof of ownership and authenticity. However, creating NFTs can be a complex and expensive process that requires technical knowledge and investment. This is where lazy minting NFTs come in, providing a cost-effective and flexible approach to NFT creation.

Let's get started!

What are Lazy Minting NFTs?

Lazy minting NFTs are a new type of NFT that allows creators to mint NFTs on-demand without the upfront costs and technical knowledge required in traditional NFT creation. Traditional NFT creation requires the creator to mint a certain number of NFTs upfront, which can be expensive and requires technical knowledge of blockchain technology. Lazy minting NFTs, on the other hand, allow creators to mint NFTs only when they are sold, reducing the upfront costs and technical requirements.

Lazy minting NFTs are created using smart contracts on a blockchain network. These smart contracts are programmed to only mint an NFT when it is sold, allowing creators to create and sell NFTs without the upfront costs and technical requirements of traditional NFT creation.

Benefits of Lazy Minting NFTs

  • Cost-effective: Lazy minting NFTs eliminate the need for upfront costs, making NFT creation more accessible to creators of all levels.

  • Flexible: Creators can mint NFTs on-demand, allowing them to create and sell NFTs without having to commit to a certain number of NFTs upfront.

  • Scalable: Lazy minting NFTs can be sold on a global scale, providing a scalable and efficient way to sell NFTs to a wider audience.

🗈 Prerequisites

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

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

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

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

⚠️ Requirements

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

Let's start building the future together!

tip

To learn more about setting up the environment to create Lazy Mint NFTs , check out the step-by-step guide to deploying your first full-stack dApp on Celo, which provides detailed instructions on getting started.

How to Create Lazy Minting NFTs

Creating lazy minting NFTs requires some technical knowledge of smart contracts and blockchain technology. Here are the steps to create lazy minting NFTs:

  • Step 1: Create a Smart Contract The first step in creating lazy minting NFTs is to create a smart contract on a blockchain network. The smart contract should be programmed to only mint an NFT when it is sold.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "@openzeppelin/contracts@4.7.3/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts@4.7.3/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts@4.7.3/access/Ownable.sol";
import "@openzeppelin/contracts@4.7.3/utils/cryptography/draft-EIP712.sol";

contract LazyNFT is ERC721, ERC721URIStorage, Ownable, EIP712 {
string private constant SIGNING_DOMAIN = "Voucher-Domain";
string private constant SIGNATURE_VERSION = "1";
address public minter;

constructor(address _minter) ERC721("LazyNFT", "LNFT") EIP712(SIGNING_DOMAIN, SIGNATURE_VERSION) {
minter = _minter;
}

struct LazyNFTVoucher {
uint256 tokenId;
uint256 price;
string uri;
address buyer;
bytes signature;
}

function recover(LazyNFTVoucher calldata voucher) public view returns (address) {
bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(
keccak256("LazyNFTVoucher(uint256 tokenId,uint256 price,string uri,address buyer)"),
voucher.tokenId,
voucher.price,
keccak256(bytes(voucher.uri)),
voucher.buyer
)));
address signer = ECDSA.recover(digest, voucher.signature);
return signer;
}

function safeMint(LazyNFTVoucher calldata voucher)
public
payable
{
require(minter == recover(voucher), "Wrong signature.");
require(msg.value >= voucher.price, "Not enough ether sent.");
_safeMint(voucher.buyer, voucher.tokenId);
_setTokenURI(voucher.tokenId, voucher.uri);
}

// The following functions are overrides required by Solidity.

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

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

The LazyNFT contract inherits from four other contracts: ERC721, ERC721URIStorage, Ownable, and EIP712.

  • ERC721 and ERC721URIStorage are OpenZeppelin contracts that define the standard for non-fungible tokens on the Ethereum blockchain. The ERC721URIStorage extension allows for storing metadata about each token on the blockchain.

  • Ownable is an OpenZeppelin contract that provides basic authorization control functions. It allows the owner of the contract to restrict access to certain functions.

  • EIP712 is an OpenZeppelin contract that provides an implementation of the Ethereum Improvement Proposal (EIP) 712, which is a standard for typed data hashing and signing.

  • The LazyNFT contract has a constructor function that takes an address _minter as a parameter and initializes the minter variable with that value. The minter variable represents the address that is authorized to mint new NFTs.

  • The contract defines a struct called LazyNFTVoucher that contains five variables: tokenId, price, uri, buyer, and signature. This struct represents a voucher that can be redeemed for a LazyNFT token.

  • The contract also defines a recover function that takes a LazyNFTVoucher as input and returns the address of the person who signed the voucher. This function is used to verify that the voucher was signed by the authorized minter.

  • The safeMint function takes a LazyNFTVoucher as input, and it checks that the voucher was signed by the authorized minter and that the buyer sent enough ether to cover the price of the NFT. If both conditions are met, the function mints a new NFT and assigns it to the buyer's address.

Finally, the contract overrides two functions from the ERC721 and ERC721URIStorage contracts: _burn and tokenURI. These functions are required by the Solidity compiler.

  • Step 2: Deploy the Smart Contract Once the smart contract is created, it needs to be deployed on a blockchain network. There are several blockchain networks that support NFT creation, including Ethereum, Binance Smart Chain, and Solana.

Deploying a smart contract requires the use of a blockchain wallet and a smart contract deployment tool. Some popular tools include Remix, Truffle, and Hardhat. These tools allow you to deploy your smart contract to the blockchain network of your choice.

tip

To learn more about setting up the environment to create Lazy Mint NFTs , check out the step-by-step guide to deploying your first full-stack dApp on Celo, which provides detailed instructions on getting started.

  • Step 3: Create and Sell Lazy Minting NFTs Once the smart contract is deployed, you can create and sell lazy minting NFTs. To create an NFT, simply call the mintNFT function on the smart contract with [voucher.tokenId,voucher.price,voucher.uri,voucher.buyer,voucher.signature].
LazyNFT nft = LazyNFT(addressOfDeployedContract);
nft.safeMint(
[voucher.tokenId,voucher.price,voucher.uri,voucher.buyer,voucher.signature], { from: walletAddress }
);

When the NFT is sold, the smart contract will mint the NFT and transfer it to the buyer.

Conclusion

Lazy minting NFTs provide a cost-effective and flexible approach to NFT creation, making it more accessible to creators of all levels. By only minting NFTs when they are sold, lazy minting NFTs eliminate the need for upfront costs and technical knowledge required in traditional NFT creation. With the ability to create and sell NFTs on-demand, lazy minting NFTs provide a scalable and efficient way to sell NFTs to a wider audience.

If you are interested in learning more about lazy minting NFTs, there are several resources available online, including tutorials and documentation on smart contract development and NFT creation.

About Author

Hi! My name is Kunal Dawar and I am a Full Stack web2/web3 Developer. I have participated in numerous hackathons and have been fortunate enough to win many of them.

One thing that I am truly passionate about is creating things that are reliable and don't break easily. I believe that creating high-quality products is important not only for the users but also for the overall growth and success of a business.

In my free time, I enjoy learning about new technologies and staying up-to-date with the latest trends in the field. I also love to share my knowledge with others and mentor those who are interested in pursuing a career in web development.

Go back

· 23 min de lectura

Header

Introduction

In this tutorial, we will be building a dapp for a Dutch auction using Solidity, Hardhat, and React. In a Dutch auction, the price of an item starts high and gradually decreases until a buyer is found. Unlike an English auction, where the price increases with each bid, a Dutch auction can be a more efficient way to sell an item, especially if the seller wants to sell it quickly. We will cover the smart contract and user interface for the dapp and show you how to deploy and interact with the dapp on Celo.

Prerequisites

We will create a Dutch auction dapp to auction off an NFT. Therefore, we assume that you already have knowledge of how NFTs work. If you are not familiar with NFTs, please complete one of the following tutorials first:

It is recommended that you have a basic understanding of Solidity, JavaScript, and React to follow this tutorial. Familiarity with Hardhat is helpful but not necessary since the tutorial will cover the basics. Having a grasp of these technologies will make it easier to understand and follow along. However, even without prior experience, you can still learn a lot from this tutorial.

Requirements

To run and test Solidity code for this tutorial, Node.js (version >=16.0) is required. To check if Node.js is installed, run the following command in the terminal:

node -v

If Node.js is not installed or the version is outdated, you can download the latest version from the official website and install it by following the instructions provided. Once Node.js is installed, you can proceed with the tutorial.

We also need to install a wallet in the browser that works with Celo, such as MetaMask.

How Does a Dutch Auction Work?

In a Dutch auction, the price of the item being auctioned gradually decreases until someone is willing to accept the current price. This differs from a traditional auction, where the price starts low and increases gradually until someone is willing to pay the highest amount.

Here's how a Dutch auction typically works:

  1. The auctioneer sets an initial asking price that is relatively high.
  2. The auctioneer then gradually lowers the price over time until someone is willing to accept the current price.
  3. Anyone who is interested in the item indicates their willingness to purchase it by placing a bid.
  4. Once a bid is placed at the current price, the auction is complete, and the item is purchased at that price.

In a smart contract, instead of manually decreasing the price over a set period of time, we can set a discount rate (discount per second) to achieve the same result. We can calculate the current price of the item using the following formula:

current price = initial price - (discount rate * time elapsed in seconds)

Project Setup

In this tutorial, we will be creating and testing smart contracts and building a web app using Hardhat and React. Let's start with the setup for both.

Hardhat Setup

First, create a directory for this tutorial and open it in a code editor like VS Code. To set up the Hardhat project, run the following command in the terminal:

npx hardhat .

After the project setup is complete, remove all files from the contracts and test directories. Next, create three new files, DutchAuction.sol, NFT.sol in the contracts directory, and DutchAuction.js in the test directory. We must also create an .env file to store the private key of the wallet that will be used for contract deployment.

We also need to install some dependencies that will help us in the development process. These dependencies are:

  • @openzeppelin/contracts: provides us with the ERC-721 standard to create NFTs.
  • dotenv: helps us read environment variables like our private key.
  • solidity-coverage: shows us test coverage for our contracts.

To install these dependencies, run the following command in the terminal:

npm install "dotenv" "@openzeppelin/contracts" "solidity-coverage"

Make sure to add the private key for the wallet that you want to use for deployment to the .env file and have some test Celo tokens in the wallet. You can obtain some test tokens from the Alfajores token faucet.

Web App Setup

To build the web app, we will be using React.js. To create a new React project, open the terminal and run the following command:

npx create-react-app frontend

Now, let's install the packages required for the web app. We need to install the following packages:

  • ethers: This package is used to interact with smart contracts and wallets.
  • react-toastify: This package is used to show notifications on the web app.

To install the packages, run the following command:

# Change directory to `frontend`
cd frontend
# Installing ethers v5.7.2 and react-toastify
npm install "ethers@5.7.2" "react-toastify"
# Change directory back to base directory
cd ..
# PWD - base directory

NFT Contract

Now that we have set up the project, we can start by writing the NFT smart contract.

First, we will write our NFT contract, with which we are going to mint the NFT that will be auctioned. As the NFT contract is not the focus of this tutorial, simply copy the following code snippet into the NFT.sol file. If you want to learn more about NFTs, you can read the tutorials linked in the Prerequisites section.

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

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

contract NFT is ERC721URIStorage {
using Counters for Counters.Counter;
Counters.Counter private _tokenIds;

constructor() ERC721("GameItem", "ITM") {}

function awardItem(
address player,
string memory tokenURI
) public returns (uint256) {
uint256 newItemId = _tokenIds.current();
_mint(player, newItemId);
_setTokenURI(newItemId, tokenURI);

_tokenIds.increment();
return newItemId;
}
}

When we put the NFT up for auction, we will give permissions to the auction contract to transfer the NFT from our wallet. The next step is to write the auction contract.

Auction Contract

To create an auction smart contract, we start by opening the DutchAuction.sol file and pasting the following code snippet that defines the smart contract outline and necessary imports:

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

import "@openzeppelin/contracts/token/ERC721/IERC721.sol";

contract DutchAuction {}

Defining Variables

We need to store some key parameters related to the auction in the contract. For each parameter, we create a variable with the following details:

  • nft: This variable is of type IERC721 and stores the address of the non-fungible token (NFT) contract that will be used to mint and auction the NFT.
  • initialPrice: This variable is of type uint256 sets the initial price of the NFT in ether.
  • discountRate: This variable is of type uint256 sets the discount rate at which the price of the NFT will decrease per unit time.
  • duration: This variable is of type uint256 sets the duration of the auction in days.
  • sold: This variable is of type bool indicates whether the NFT has been sold or not. It is initially set to false.
  • startTime: This variable is of type uint256 stores the timestamp when the auction starts.
  • id: This variable is of type uint256 and stores the ID of the NFT that will be auctioned.
contract DutchAuction {
IERC721 public nft;

uint256 public initialPrice = 50 ether;
uint256 public discountRate = 20 wei;
uint256 public duration = 7 days;
bool public sold = false;

uint256 public startTime;
uint256 public id;

// Constructor
}

Defining Constructor

In the constructor function, we initialize three variables, namely id, startTime, and nft. We require two arguments to set these variables, _id and _nft. We pass _nft to IERC721 to set up the nft variable, while id is assigned the value of _id. Additionally, we set the value of startTime as the current timestamp at the moment of contract deployment.

    constructor(address _nft, uint256 _id) {
id = _id;
nft = IERC721(_nft);
startTime = block.timestamp;
}
// Calculate current price of the NFT

Get Current Price

We can calculate the current price of the item being auctioned in the function called getPrice. The formula for the current price is:

current price = initial price - (discount rate * time elapsed in seconds)

Time elapsed can be calculated by subtracting the current timestamp and start time. This will give us the elapsed time in seconds. We can multiply the time elapsed with discountRate and subtract the result from the initial price.

    // Calculate current price of the NFT
function getPrice() public view returns (uint256 price) {
uint256 timePassed = block.timestamp - startTime;
price = initialPrice - (timePassed * discountRate);
}

// buy NFT being auctioned

Buy NFT

We can write a function called buy that allows anyone to purchase an NFT at the current price. This function first checks whether the auction is still ongoing by verifying whether the current block timestamp is less than the end time of the auction. If the auction is still ongoing, it then calculates the current price of the NFT using the getPrice() function.

Next, the function checks whether the value sent with the transaction is greater than or equal to the calculated price. If this condition is met, the function transfers the NFT from the contract owner to the caller and refunds any excess ETH sent with the transaction. Finally, the function sets the sold variable to true.

    // buy NFT being auctioned
function buy() public payable {
require(block.timestamp < startTime + duration, "Auction ended");

uint256 price = getPrice();
require(msg.value >= price, "ETH < price");

nft.safeTransferFrom(nft.ownerOf(0), msg.sender, id);
uint256 refund = msg.value - price;
if (refund > 0) {
payable(msg.sender).transfer(refund);
}
sold = true;
}

Now that we've completed our smart contract, we can move on to unit testing it.

Unit Tests

It is considered a good practice to write unit tests for all smart contracts to ensure that they work according to our expectations. In this section, we will write unit tests for our smart contract.

To begin, we import necessary dependencies and create a describe block containing all the tests that we will write. The describe block is a way to group tests together.

const {
days,
} = require("@nomicfoundation/hardhat-network-helpers/dist/src/helpers/time/duration");
const {
loadFixture,
time,
} = require("@nomicfoundation/hardhat-network-helpers");
const { expect } = require("chai");
const hre = require("hardhat");

describe("Dutch Auction", async () => {
const deployFixture = async () => {};
});

We need to define the deployFixture function first. The fixture will deploy both smart contracts and approve the auction contract as an operator for the NFT that is being auctioned. Additionally, we will create two dummy accounts that will be used for testing purposes.

After creating the smart contracts, we will return instances of both the contracts and the two dummy addresses.

const deployFixture = async () => {
const [owner, buyer] = await hre.ethers.getSigners();
let nft;

try {
const NFT = await hre.ethers.getContractFactory("NFT");
nft = await NFT.deploy();

await nft.deployed();
await nft.awardItem(
owner.address,
"https://gist.githubusercontent.com/nikbhintade/97994377f414de00809dad098ca57bf2/raw/117c9db714b13425116c2496ed0b237e7c88d83b/nft.json"
);
} catch (err) {
console.log(err);
}

let dutchauction;

try {
const DutchAuction = await hre.ethers.getContractFactory("DutchAuction");
dutchauction = await DutchAuction.deploy(nft.address, 0);

await dutchauction.deployed();
} catch (err) {
console.log(err);
}

await nft.approve(dutchauction.address, 0);
return { dutchauction, nft, owner, buyer };
};

Our first test will check if the contracts are deployed correctly by verifying that the variables we set from the constructor have the expected values and that the auction contract is approved as an operator for the NFT being auctioned.

it("should deploy contract set using correct parameters", async () => {
const { dutchauction, nft } = await loadFixture(deployFixture);

expect(await dutchauction.id()).to.be.equal(0);
expect(await dutchauction.startTime()).to.be.equal((await time.latest()) - 1);
expect(await dutchauction.nft()).to.be.equal(nft.address);

expect(await nft.getApproved(0)).to.be.equal(dutchauction.address);
});

We have learned about how the contract calculates the current price. Now, we will test if it accurately calculates the current price in the following test:

it("should return the correct price", async () => {
const { dutchauction } = await loadFixture(deployFixture);

const DISCOUNT_RATE = await dutchauction.discountRate();
const INITIAL_PRICE = await dutchauction.initialPrice();

await time.increase(3600 - 1);

const PRICE = hre.ethers.BigNumber.from(3600).mul(DISCOUNT_RATE);

expect(await dutchauction.getPrice()).to.be.equal(INITIAL_PRICE.sub(PRICE));
});

This test ensures that the getPrice() function accurately returns the current price by comparing the expected price, calculated based on the discount rate and initial price, with the actual price returned by the function.

Next, let's test if the buy functionality works as expected. The expected behavior is that if a buyer buys the NFT at the current price, then the NFT will be transferred to the buyer's wallet. To test this, we will check if the Transfer event is emitted from the NFT contract with the correct arguments. The test code is as follows:

it("should allow the buying of the NFT", async () => {
const { dutchauction, nft, buyer } = await loadFixture(deployFixture);

await time.increase(3600 - 1);
const PRICE = await dutchauction.getPrice();

expect(await dutchauction.connect(buyer).buy({ value: PRICE }))
.to.emit(nft, "Transfer")
.withArgs(dutchauction.address, buyer.address, 0);
});

The buy() function also includes a refund option that refunds the buyer if they paid more than the current price. To test this functionality, we will send more ether than the current price and check if the balance of the smart contract is equal to the current price of the NFT, as the remaining amount will be refunded to the buyer. The test code is as follows:

it("should refund the extra amount", async () => {
const { dutchauction, buyer } = await loadFixture(deployFixture);

await time.increase(3600);

await dutchauction.connect(buyer).buy({ value: "999999999999999999999" });
const PRICE = await dutchauction.getPrice();

expect(
await hre.ethers.provider.getBalance(dutchauction.address)
).to.be.equal(hre.ethers.BigNumber.from(PRICE));
});

The above test checks if the extra amount paid is refunded to the buyer by comparing the balance of the smart contract to the current price of the NFT.

Next, let's test if the smart contract restricts the buying of the NFT after the end of the auction. To test this, we will increase the timestamp by 7 days and then call the buy() function. It should revert with the reason "Auction ended". The test code is as follows:

it("should not allow buying after the auction ends", async () => {
const { dutchauction, buyer } = await loadFixture(deployFixture);

await time.increase(days(7));

const PRICE = await dutchauction.getPrice();
await expect(
dutchauction.connect(buyer).buy({ value: PRICE })
).to.revertedWith("Auction ended");
});

We also need to test if the buyer can only buy the NFT at the current price or higher. To test this, we'll pay less than the current price and check if the function reverts with the reason "ETH < price". Here's the code:

it("should not allow buying if less price is paid", async () => {
const { dutchauction, buyer } = await loadFixture(deployFixture);

await time.increase(3600);

const PRICE = await dutchauction.getPrice();
await expect(
dutchauction.connect(buyer).buy({ value: PRICE.sub(1000) })
).to.be.revertedWith("ETH < price");
});

With these tests, we have covered most of the contract. We can now set up our project to see the code coverage. To set up the solidity-coverage plugin, we just need to import it in hardhat.config.js.

require("solidity-coverage");

To run the tests and see the coverage, we can run the following command in the terminal:

npx hardhat coverage

The output of the coverage should look something like this:

Coverage Output

Now that we have written and tested the auction smart contract, let's proceed to deploy both of our smart contracts.

Contract Deployment

Firstly, we need to add the network configuration for the Alfajores network and import dotenv to hardhat.config.js. The import statement will be at the top of the file, and the Alfajores configuration will go in the module.exports object:

require("@nomicfoundation/hardhat-toolbox");
require("solidity-coverage");
require("dotenv").config();

/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
solidity: "0.8.17",
defaultNetwork: "hardhat",
networks: {
hardhat: {},
alfajores: {
url: "https://alfajores-forno.celo-testnet.org",
accounts: [process.env.PRIVATE_KEY],
chainId: 44787,
},
celo: {
url: "https://forno.celo.org",
accounts: [process.env.PRIVATE_KEY],
chainId: 42220,
},
},
};

We have configured our project to deploy to the Alfajores testnet. To deploy both of our smart contracts, we need to write a script. Open deploy.js from the scripts directory. Remove all the content from the main function and add the following snippet:

const hre = require("hardhat");

async function main() {
const deployer = await hre.ethers.getSigner();
console.log(deployer.address);

const NFT = await hre.ethers.getContractFactory("NFT");
const nft = await NFT.deploy();
await nft.deployed();

console.log(`NFT contract address is ${nft.address}`);

const mintTxn = await nft.awardItem(
deployer.address,
"https://gist.githubusercontent.com/nikbhintade/97994377f414de00809dad098ca57bf2/raw/137da789cb4ba9710db60439c27e9c36deeee555/nft.json"
);
await mintTxn.wait();

const DutchAuction = await hre.ethers.getContractFactory("DutchAuction");
const dutchAuction = await DutchAuction.deploy(nft.address, 0);
await dutchAuction.deployed();

console.log(`Dutch Auction contract is ${dutchAuction.address}`);

const approvalTxn = await nft.approve(dutchAuction.address, 0);
await approvalTxn.wait();
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

In this script, we are performing the same steps as we did when creating a fixture for our unit tests. Firstly, we deploy the NFT contract and mint an NFT with the contract deployer's wallet as the owner. Next, we deploy the auction contract with the correct parameters, and finally, we approve the auction contract as an operator for the minted NFT.

With the setup for contract deployment completed, you can run the following command in the terminal to deploy the contracts:

npx hardhat run scripts/deploy.js --network alfajores

You will see an output similar to the following:

Deployment output

Creating The Web App

As we have tested and deployed our smart contract, we can proceed with building the web app. To create the web app, we need the contract ABI. We have generated the ABI while deployed contracts. Copy the DutchAuction.json and NFT.json from the artifacts/contracts/DutchAuction.sol and artifacts/contracts/NFT.sol directory to frontend/src.

To start building the web app, open the App.js file in the frontend/src directory and paste the following code snippet. It creates the basic structure of the web app:

import "./App.css";
import DutchAuction from "./DutchAuction.json";
import NFT from "./NFT.json";

import { useEffect, useState } from "react";
import { ethers } from "ethers";
import { ToastContainer, toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";

const CONTRACTADDRESS = "0x60b9752bF3e616f4Aa40e12cdB6B615fe5e14807";

function App() {
const [state, setState] = useState({
contract: undefined,