How to build a thrift smart contract on Celo leveraging Witnet Oracle Randomness function

How to build a thrift smart contract on Celo leveraging Witnet Oracle Randomness function https://celo.academy/uploads/default/optimized/2X/6/668de83644bc753422bd091168fecb71eecdc7f0_2_1024x576.png
none 4.0 1

Introduction

In this tutorial, we will look into what a thrift app is and how we can build a smart contract that brings the traditional africa thrift institution to the blockchain. So this tutorial will:

  • Explain what thrift is
  • Explain how we can build a smart contract that mimics the behaviour of a thrift institution

Prerequisites

In other to follow through with this tutorial, the developer or builder needs to have foreground knowledge of:

  • How blockchain and smart contracts work
  • Programming with solidity and deploying smart contracts on the celo blockchain

Requirements

To test the functionality of the code used in this tutorial developer can use the following tool

  • Remix IDE or other suitable IDE to write, deploy and test the smart contract
  • Metamask configured with Celo Alfajore network
  • Some Celo testnet tokens from the faucet

Thrift and how they work

We will start with what a thrift is and how the traditional thrift institution works from there, we will look into some UI concept for the thrift smart contract frontend application. Lastly, we will look at the smart contract that mimics the behavior of a thrift institution and possible functionalities that can be included later on.

Thrift is an informal savings and lending mechanism prevalent in many African countries. These informal systems often involve community-based savings groups, rotating savings and credit associations (ROSCAs), or microfinance institutions that cater to the needs of underserved populations. These mechanisms are sometimes referred to as “thrift societies” or “savings clubs.”

In these systems, individuals pool their savings together and take turns receiving lump-sum amounts from the collective pool. This enables members to access funds for various purposes, such as starting a small business, paying for education, or addressing emergency expenses. These informal savings and lending practices serve as an important source of financial support and social cohesion within communities, particularly in areas where formal banking services may be limited. An example of a thrift institution is farmers coming together to create a saving club where any member of the club can save together, access fund from the group to buy farm inputs or to respond to an emergency like disease outbrs on the farm.

The Celo Thrift App UI

The celo thrift app will demonstrate how user groups like farmers association, can be created on the Celo blockchain. This user group is called a club which will hold a specific number of users. Each user will stake their fund at a set interval at the same time users can also request funds from the club to address their needs.

So the starting point is for users mostly people who already know each other to come together to create a new club or group to cater for their common needs. In this case, a user can initiate creating a new club as shown in the UI flow above. After creating a new club, users can be included by adding their wallet during the creation process or by a user searching for a club shown above and joining. After joining users can start saving their crypto by staking weekly, monthly or as decided by the members to support the goal of the club. The interval for re-staking also referred to as saving in the traditional context, can be hard coded into the smart contract or can be adjusted by the club creator

After joining a thrift club and the total number of users in a club have staked their tokens either cUSD or other options the thrift round will start. A round is a process where every user in a club continuously stake tokens at set intervals and can request funds when it is their turn. A user is allowed to remove funds after a position number between 1 and the Nth number of total users in the club is assigned at the start of the round. So if a user is in position 12 in a round the user can only request withdrawal only after the contract counts to 12.

After every user in the club has pulled from the club and also continuously staked their token the round can end and a new round initiated. The next session explains the code implementation in a smart contract.

Smart Contract Implementation

The smart contract consists of functions, structs and state variables. The first part of the code is a thrift contract itself which holds the different functions and data in the smart contract as shown below

//solidity
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.15;
Import "witnet-solidity-bridge/contracts/interfaces/IWitnetRandomness.sol";


contract Thrift{
   
    struct Club{
        string clubName;
    }
    struct Users{
     
        string userName;
        address userAddress;  
    }
   
    Users[] public user;
   


    mapping(string =>  Users[]) public clubToUser;
    mapping(string => uint256) public nameToAmount;
    mapping(string => uint256) public numberOfUsers;
    mapping(address => bool) public hasPaid;
    mapping(string => uint256 ) public totalAmountInClub;
    mapping(string => uint256) public trackOfUsersThatPaid;
      uint32 public randomness;
      uint time ;
    uint256 public latestRandomizingBlock;
    IWitnetRandomness immutable public witnet;
   
    /// @param _witnetRandomness Address of the WitnetRandomness contract.
    //This is Witnet Randomness Contract address that you would want to put as per your desired blockchain network given by https://docs.witnet.io/smart-contracts/supported-chains
    constructor (IWitnetRandomness _witnetRandomness) {
        assert(address(_witnetRandomness) != address(0));
        witnet = _witnetRandomness;
 time = block.timestamp;
    }
   
    receive () external payable {}


    function requestRandomNumber() external payable {
        latestRandomizingBlock = block.number;
        uint _usedFunds = witnet.randomize{ value: msg.value }();
        if (_usedFunds < msg.value) {
            payable(msg.sender).transfer(msg.value - _usedFunds);
        }
        time = block.timestamp;
    }
   
    function fetchRandomNumber(string memory _clubName)  public{
         assert(latestRandomizingBlock > 0);
            Users[] memory myarr = clubToUser[_clubName];
uint32 num = uint32(myarr.length);
   
     
     
 randomness = witnet.random(num , 0, latestRandomizingBlock);
    }
   


    function createClub(string memory _clubName, string memory _userName) public{
       
        Users memory clubUser =  Users(_userName, msg.sender);
         numberOfUsers[_clubName] = 1;
        clubToUser[_clubName].push(clubUser);
       
   
    }
    function addUser(string memory _clubName , string memory _userName, address _user) public{
        require(clubToUser[_clubName].length>0, "Club Not Found");
        Users memory clubUser = Users(_userName, _user);
         numberOfUsers[_clubName] += 1;
        clubToUser[_clubName].push(clubUser);
       
    }
 


    function proposedContirbutionAmount(string memory _clubName, uint256 _proposedAmount) public {
     uint256 amountInEther= _proposedAmount*10**18;
     nameToAmount[_clubName] = amountInEther;


    }
    function intervalInWeeks(string memory _clubName) public view returns(uint256){
      return numberOfUsers[_clubName];
    }


    function getContractBalance() public view returns (uint256) {
        return address(this).balance;
    }
    function getAmountFromClub(string memory _clubName) public view returns(uint256){
        return nameToAmount[_clubName];
    }


    function sendAmountToContract(string memory _clubName) public payable{
        uint256 amount = getAmountFromClub(_clubName);
        // uint numberOfUsersInTheClub = numberOfUsers[_clubName];
        require(msg.value == amount , "please pay the correct amount" );
        require(hasPaid[msg.sender] == false , "User has already paid" );
        hasPaid[msg.sender] = true;
        trackOfUsersThatPaid[_clubName] += 1;
        totalAmountInClub[_clubName] += msg.value;
       
    }
    function hasEveryOnePaid (string memory _clubName) public returns (bool){
        return trackOfUsersThatPaid[_clubName] == numberOfUsers[_clubName];
    }
    function withdraw(string memory _clubName) public {
     require(block.timestamp > time + 600, "Please wait for 10 minutes until next transaction");
     require (hasPaid[msg.sender] == true , "User has not paid" );
        Users storage newArray = clubToUser[_clubName][randomness];
     fetchRandomNumber(_clubName);
     address payable _receiver = payable( newArray.userAddress);
     _receiver.transfer(totalAmountInClub[_clubName]);
     time = block.timestamp;
}
}

After creating the thrift contract by declaration, the next line of code creates a struct type to hold the club name, user addresses, and username and a mapping function that does the following:

  • Number of users in the club
  • Name of users and how much they are staking
  • Total number of users in the club
  • Map users who have staked or not in a club
  • Track users who have paid

While deploying the contract, the constructor requires the contract address of the Witnet Randomness contract that has been deployed on Multiple Mainnet as well as Testnet chains

//solidity
 struct Club{
        string clubName;
    }
    struct Users{
     
        string userName;
        address userAddress;  
    }
   
    Users[] public user;
   


    mapping(string =>  Users[]) public clubToUser;
    mapping(string => uint256) public nameToAmount;
    mapping(string => uint256) public numberOfUsers;
    mapping(address => bool) public hasPaid;
    mapping(string => uint256 ) public totalAmountInClub;
    mapping(string => uint256) public trackOfUsersThatPaid;
      uint32 public randomness;
      uint time ;
    uint256 public latestRandomizingBlock;
    IWitnetRandomness immutable public witnet;
   
    /// @param _witnetRandomness Address of the WitnetRandomness contract.
    //This is Witnet Randomness Contract address that you would want to put as per your desired blockchain network given by https://docs.witnet.io/smart-contracts/supported-chains
    constructor (IWitnetRandomness _witnetRandomness) {
        assert(address(_witnetRandomness) != address(0));
        witnet = _witnetRandomness;
 time = block.timestamp;
    }

Function createClub takes two inputs, FIrst - Desired clubName and the Second one is the userName. It creates Club with the User Details stored in the struct and the same struct of the user is mapped over the clubName with the mapping name clubToUser.

//solidity
function createClub(string memory _clubName, string memory _userName) public{      
        Users memory clubUser =  Users(_userName, msg.sender);
         numberOfUsers[_clubName] = 1;
        clubToUser[_clubName].push(clubUser);   
    }

Function addUser() allows users to add themselves to any preexisting club by taking in the inputs as follows

//solidity
string memory _clubName , string memory _userName, address _user

Be sure to enter a Valid ClubName that already exists otherwise, it will show an error.

//solidity
function addUser(string memory _clubName , string memory _userName, address _user) public{
        require(clubToUser[_clubName].length>0, "Club Not Found");
        Users memory clubUser = Users(_userName, _user);
         numberOfUsers[_clubName] += 1;
        clubToUser[_clubName].push(clubUser);
       
    }

The below function allows users to propose a contribution amount that each and every user agrees to pay to the club at certain time intervals. It takes the clubName and the amount as input (in ethers).

//solidity
 function proposedContirbutionAmount(string memory _clubName, uint256 _proposedAmount)

 function proposedContirbutionAmount(string memory _clubName, uint256 _proposedAmount) public {
     uint256 amountInEther= _proposedAmount*10**18;
     nameToAmount[_clubName] = amountInEther;

    }

These are some getter functions which give the contract balance as well as how much amount of money the club holds.

//solidity
function getContractBalance() public view returns (uint256) {
        return address(this).balance;
    }
    function getAmountFromClub(string memory _clubName) public view returns(uint256){
        return nameToAmount[_clubName];
    }

This function (payable) enables the user to send an amount to the Contract that has been proposed before by all the club members. It first gets the amount that has been proposed, and checks if the amount that is being sent is equal to the proposed amount and if the user has paid before or not.
Once all the conditions are passed the amount is sent to the contract.

//solidity
function sendAmountToContract(string memory _clubName) public payable{
        uint256 amount = getAmountFromClub(_clubName);
        // uint numberOfUsersInTheClub = numberOfUsers[_clubName];
        require(msg.value == amount , "please pay the correct amount" );
        require(hasPaid[msg.sender] == false , "User has already paid" );
        hasPaid[msg.sender] = true;
        trackOfUsersThatPaid[_clubName] += 1;
        totalAmountInClub[_clubName] += msg.value;
       
    }

This function keeps the record of every user that paid to the contract.

 function hasEveryOnePaid (string memory _clubName) public returns (bool){
        return trackOfUsersThatPaid[_clubName] == numberOfUsers[_clubName];
    }

Withdraw funds from the Club

Once all the users have staked the amount in the club, the withdraw function can be run with the argument of _clubName which first requests a random number from Witnet Oracle( note: function requestRandomNumber() needs to be triggered every time before running the withdraw function) from the total number of users present in the club, and it pays all the amount contributed by the club members to the random member of the club. If a particular user has not contributed to the club, he/she will not be eligible to withdraw money Club. There is a timeout to request a random number from the Witnet Oracle of 10 minutes.

//solidity
function withdraw(string memory _clubName) public {
     require(block.timestamp > time + 600, "Please wait for 10 minutes until next transaction");
     require (hasPaid[msg.sender] == true , "User has not paid" );
        Users storage newArray = clubToUser[_clubName][randomness];
     fetchRandomNumber(_clubName);
     address payable _receiver = payable( newArray.userAddress);
     _receiver.transfer(totalAmountInClub[_clubName]);
     time = block.timestamp;


    }

Result

After compiling the code here are some screenshots of the test result from interaction with the
Smart Contract

Conclusion

In this tutorial, we were able to cover how we can create a smart contract that uses the Witnet oracle function to create a thrift-saving application that runs on the Celo blockchain. The tutorial is meant to be an eye-opener for developers to see the potential use of blockchain technology to replace existing traditional financing tools/services thus creating a more inclusive future. It is also very important to note that developers referencing this tutorial can have functionalities such as a credit rating system for users, flexible staking intervals, and reward mechanisms, among many other things.

Next Steps

The next step for developers is to learn more about building frontend applications on Celo to integrate with the smart contract developed. The required resources to get started can be found in the Celo documentation and also in tutorials available in the Celo Academy. Happy learning!

About the Author

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

References

3 Likes

Hi @fmoney I’ll be reviewing your piece

2 Likes

This looks really good I’m moving to publish

1 Like

Hi @fmoney I forgot to mention, can you add a lin to the tutorial’s codebase on github

1 Like

Check. GitHub link has been included

what line

Check the reference

Hey @fmoney I hope this message finds you well, We are happy to see your recent submission, but unfortunately we had to hold off this for now, because

  1. The title says , How to build a dApp, but there is no mention of the frontend part here
  2. Your github links are not working.

I would like to get this topic back on board as soon as possible

For now, we kindly request that you hold off on this post. We encourage you to revisit our guidelines, and you’re always welcome to modify your proposal and submit it again at your convenience.

Thank you for your understanding and collaboration. If you have any questions, don’t hesitate to reach out.

Best,

Ishan Pathak
Celo Academy Team

@ishan.pathak2711
I will need to edit the title as my intention is to create a smart contract that mimics the functionality of a thrift system with UI to illustrate how the frontend can be implemented

The final decision will be of @joenyzio

Hey @fmoney please revise the title and mentions of dApp to match the scope of the tutorial, avoid using tags in your proposals for languages you don’t use (ex. React), and update your GitHub link (it’s 404 right now). Thanks!

1 Like

Done
@joenyzio @Phenzic kindly review again

1 Like

Publishing this tutorial :seedling:

2 Likes

witnet oracle network literally provides a secured way to retrieve data from the real world and bring into blockchain , definitely crucial for building a thrift app

3 Likes

I would love to read this through. I will tender my review later on.

2 Likes