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
- Visual Studio Code as code editor
- Github as Source Reposirory
- Hardhat as EVM development evironment
- Chai to help on tests
Solution π
- Keep wallets addresses that needs to receive values in the contract and the total of active wallets
- Store how much each wallet has totally withdraw; With that we can include it on the calculation of how much each wallet can withdraw
- 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)
- Store total of value that contract has
- 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";
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;
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]);
}
}
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;
}
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;
}
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;
}
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);
}
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;
}
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;
}
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
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',
},
}
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.
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'
)
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)
'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
network name should be the same as we configured in the hardhat.config.js
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
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.
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
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
Now if we check again the bsc scan..
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
Hope you liked it!
Here is the repo in github used
Did it help you? Feel free to comment and share π
Top comments (0)