DEV Community

Cover image for Smart Contract for division
marcoslobo
marcoslobo

Posted on

Smart Contract for division

Story 😴

In one of my freelancers jobs, I saw that the project was needing another smart contract for divide fees that other contract generates when clients used it (like 10% for dev, 10% for marketing..) and the client was doing it manually transfering to one wallet and then sharing it once a week.

Use case πŸ“–

So basically, I needed a smart contract to receive values in it's address(without calling an function, just 'receiving' transfers) and then allows specified wallets to withdraw it and costing the less fee as possible to the main function from client.

Tools used πŸ› οΈ

Nowadays, for solidity i'm using

Solution πŸ’­

  1. Keep wallets addresses that needs to receive values in the contract and the total of active wallets
  2. Store how much each wallet has totally withdraw; With that we can include it on the calculation of how much each wallet can withdraw
  3. Block & unblock wallets storing it on total of wallets(when blocked, total of wallets to divide -1, when unblock, total of wallets to divide +1)
  4. Store total of value that contract has
  5. With the total that the contract has (calculate when receive value and when withdraw value), total of wallets to divide and how much each wallet has withdraw, we can calculate how much an specific wallet can withdraw

Code + solution + comments 🩺

I will break the smart contract code in small peaces and try to explain it
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;

// Import this file to use console.log
import "hardhat/console.sol";

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";

Enter fullscreen mode Exit fullscreen mode

One of the cool features that I love in hardhat is the possibility to use console.log inside smart contract, for this, we just need to import the "hardhat/console.sol"
Ownable.sol import help us to control who can access specifics functions.
SafeMath.sol import help us in math calculations

contract Division is Ownable {

    struct participant {
        uint256 totalReceived;
        bool blocked;
    }

   mapping(address => participant) participants;

   uint256 totalInContract;

   uint256 public totalParticipants;

Enter fullscreen mode Exit fullscreen mode

On the declaration of the contract 'Division' we say that it 'is Ownable' and with that we are able to use functions from Ownable.sol
The struct 'participant' will allow us to know how much an address has received(totalReceived property) and if this address is blocked. How we will do that? In conjunction with the mapping.
Mapping is a list with an key and we used the address as it (1 address -> 1 struct)
totalInContract will let us know how much $ we have in contract to divide
totalParticipants will let us know how many addresses we should divide the $

  constructor(address[] memory pDivisors) {
        for (uint256 i = 0; i < pDivisors.length; i++) {
            addParticipant(pDivisors[i]);
        }
    }
Enter fullscreen mode Exit fullscreen mode

In the constructor we expect an list of addresses that will be the owners of divisions

    receive() external payable {
        if (msg.value <= 0) return;
        share(msg.value);
    }

    function share(uint256 valueToShare) private {
        totalInContract += valueToShare;
    }

Enter fullscreen mode Exit fullscreen mode

One of the magic tricks is this function 'receive()'. It allow us to do something when the contract receives an value.
The share function is very simple, we just increment the totalInContract with the received value.(It don't loop all addresses in the mapping, if it did that, the gas cost will be expensiver)

    function addParticipant(address participant_) public onlyOwner {
        participants[participant_] = participant(0, false);
        totalParticipants += 1;
    }

Enter fullscreen mode Exit fullscreen mode

This function allows to add an participant(to receive part of division).
The onlyOwner modifier garantees that just the Owner will be able to use this function

    function getValueToShareForAddress(address participant_)
        private
        view
        returns (uint256)
    {
        if (participants[participant_].blocked) return 0;

        uint256 valueToShare = totalInContract / totalParticipants;
        return valueToShare - participants[participant_].totalReceived;
    }

Enter fullscreen mode Exit fullscreen mode

This function checks if the user is blocked, and if he's, no cash for him.
One good point is that we calculate the value shared for all, then we use the total received from the address to check how much it can receive

    function withdraw() public payable {
        address msgSender = _msgSender();

        uint256 valueToShare = getValueToShareForAddress(msgSender);

        require(valueToShare > 0, 'you dont have funds for withdraw');

        participants[msgSender].totalReceived += valueToShare;

        payable(msgSender).transfer(valueToShare);
    }

Enter fullscreen mode Exit fullscreen mode

This function is for the address that wants to receive his dividens. It uses the function getValueToShareForAddress to get the value and if it's 0 it throws an error. If the user has something to withdraw, then we increment the totalReceived from this address then transfer it to him

 function getBalance() external view returns (uint256) {
        return getValueToShareForAddress(_msgSender());
    }

  function getDivisorsQuantity() external view returns (uint256) {
        return totalParticipants;
    }

Enter fullscreen mode Exit fullscreen mode

This function 'getBalance' is where the user knows how much he is able to withdraw
The getDivisorsQuantity, will return how many participants enabled the contract has

    function blockParticipant(address participant_) external onlyOwner {
        participants[participant_].blocked = true;
        totalParticipants -= 1;
    }

    function unblockParticipant(address participant_) external onlyOwner {
        participants[participant_].blocked = false;
        totalParticipants += 1;
    }

Enter fullscreen mode Exit fullscreen mode

As the functions name says, it blocks or unblock an address.
A good point is that just the owner can do this
Another good point is that we always change the totalParticipants

Testing πŸ§ͺ

To test if everything is working as it should we need to do good tests before deploy our contract. Off course we could deploy to an testnet and play there(I do it too before mainnet), but writing tests and run it with hardhat is so easy and fast that you gonna love it.
To run the tests just type

npx hardhat test

Hardhat will look for .js files in /test folder and execute then

tests runned 100%

I will make an post just talking about hardhat tests and how it's important

Finally... Deploy ! πŸš€

Hardhat helps us in this mission too. First, you need to configure what blockchain and network you want to deploy. Check the hardhat.config.js for this.

require('@nomiclabs/hardhat-waffle')
require('dotenv').config()
require('@nomiclabs/hardhat-etherscan')

module.exports = {
    solidity: '0.8.14',
    networks: {
        bsc_testnet: {
            url: `${process.env.BSC_TESTNET_URL}`,
            accounts: [`${process.env.BSC_TESTNET_PRIVATE_KEY}`],
        },
    },
    etherscan: {
        apiKey: 'xxx',
    },
}
Enter fullscreen mode Exit fullscreen mode

Networks section is where you configure the blockchains and networks that you want to deploy. In this case, let's say that we want to deploy on Binance Smart Chain.
For this I named this blockchain/network as bsc_testnet.
We need to inform an Url of node to the desired BC/Network. I I usually use free nodes for deploy, you can get an free account on this providers(quicknode nownodes).
We will need the Private Key from the deployer account, this account will be the owner of the Smart Contract and it will be the payer of the deploy fees.
The apiKey parameter we will talk on the end of deploy

One good practice is to store those sensitive values in a .env file, and for this the package

require('dotenv').config()

helps us, getting the file from root directory from project. I placed an sample.env that you can copy, rename to .env and adjust the variables.

Where and how .env should be

For BSC Testnet, we can use an public node https://data-seed-prebsc-1-s1.binance.org:8545

The next step is configure the scripts/deploy.js file

I will break the file in small peaces and try to explain it
const hre = require('hardhat')

async function main() {
    const [owner] = await hre.ethers.getSigners()

    const divisiondContractFactory = await hre.ethers.getContractFactory(
        'Division'
    )

Enter fullscreen mode Exit fullscreen mode

Here 'hre' is our magic guy coming from hardhat package and with him we get the owner address(the deployer)
The 'divisiondContractFactory' will get our contract by his name.

let divisors = [
        '0x64eb4Abe091301Fb7403bb1b19159b80ea1Fac29',
        '0x3e10abf5d5F9D8396fc027DCbf00Fae1b9F9F160',
    ]

   const divisionContract = await divisiondContractFactory.deploy(divisors).deployed();    

    console.log('Le Division deployed to:', divisionContract.address)

    console.log('Le Division owner address:', owner.address)
Enter fullscreen mode Exit fullscreen mode

'divisors' is an array of adresses that you want to be the initial winners from divisions(you can add more or block after deploy)
With the contract factory from our contract, we give to it the parameters then deploy the contract. In the end of all we get the contract and owner address.
To finally deploy the contract you just need to run

npx hardhat run .\scripts\deploy.js --network bsc_testnet
Enter fullscreen mode Exit fullscreen mode

network name should be the same as we configured in the hardhat.config.js

Contract deploy command after executed
VoualΓ‘! Here is our contract address. You can check it at here
One detail.. Remember about verify our contract? Check how it stays after deploy

Contract bytecoded on scan
Bytecoded! To solve this, we need to configure our API key on hardhat.config file. In most blockchains, you can get it by creating an account on the explorer and going into API-KEYs.

Explorer page with api key

After getting you apiKey and replacing it on hardhat.config, run the verify command:

npx hardhat verify --network bsc_testnet  --constructor-args arguments.js 0x671D185d3cD266914A8a5C97e436500003D8f1B5
Enter fullscreen mode Exit fullscreen mode

again network name should be the same as we configured in the hardhat.config.js
the next argument after network is about the parameters that we used in the deploy. As we used array we can pass it directaly on the command, for this we will use the constructor-args

Verify command success
Now if we check again the bsc scan..

bsc scan ok!

Ow yes !!
Now we got 3 tabs on the contract, Code, Read and Write.
You can use all the function of the contract directly on the explorer. \o

yes!

Hope you liked it!
Here is the repo in github used
Did it help you? Feel free to comment and share πŸ™ƒ

Top comments (0)