Skip to main content

Introduction

This guide will show you how to write a simple distributor contract in Solidity. The contract contains one function with the name distribute. The sole purpose of this function is to call it with a desired amount of Zeniq which gets then distributed between the passed addresses.

The reason why this functionality is useful is that it makes smartchain faucets more scalable. The repository can be found at Distributor Contract. It includes a ready-made setup for testing and deploying a smart contract to the ZENIQ smartchain.

Work Environment

For this tutorial Hardhat is the working environment. Make sure to have it correctly installed in your workspace. Hardhat is a popular development environment for writing, testing, and deploying smart contracts. It offers a wide range of features and plugins to streamline the development process which is the reason it is used in this demonstration example. We also can recommend it for your own smartchain projects.

Initialization

Create a new directory and navigate into it. Execute the following commands in this new (and empty) directory (if you want you can make different adjustments when doing the setup, this is only one of many ways to initialize a new project):

git init
npm init -y
npm i hardhat
npx hardhat
create JS project
add .gitignore
do not install hardhat-toolbox

This will create the project and install hardhat. To interact with the smartchain one can use either Web3.js or Ethers.js. In the end this is up to the developer but for this guide ethers.js will be used as it is easier to understand and use:

npm i --save-dev [email protected]
npm i --save-dev @nomiclabs/hardhat-ethers

As you may notice the used version of ethers.js in this guide is version 5, which is not the most recent one. However, we recommend to use this version of ethers.js as version 6 came with lots of API breaking changes.

Configuration

After installing your preferred development framework, the next step is to configure it to work with ZENIQ Smart Chain. Here's an example configuration for Truffle's truffle-config.js file:

const mnemonic = 'test test test test test test test test test test test test';
const accounts = {
mnemonic,
count: 10
}

/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
solidity: {
compilers: [
{
version: "0.8.19",
settings: {
optimizer: {
enabled: true,
runs: 200000
},
viaIR: true
}
}
]
},
networks: {
localhost: {
accounts
},
hardhat: {
accounts
},
zeniq: {
accounts,
url: "https://smart.zeniq.network:9545"
}
}
};
caution

In general, make sure to use the most recent version of solidity when creating new contracts. However, at the time this documentation is written, solidity v0.8.21 is already released, but it is recommended to use v0.8.19 for the following reason:

The solidity compiler takes the smart contract code and compiles it to assembly code, specifically designed for the EVM running inside smart chains. This EVM gets updated from time to time by the Ethereum Foundation, and since Ethereum is the biggest smart chain right now, almost all relevant tools get adapted to those changes sooner or later. Looking into the solidity compiler changelog one can see that there have been changes for the new 'Shanghai' EVM version, which got released in June this year. 'Shanghai' introduced a new opcode PUSH0 which is already supported by the Hardhat EVM spun up when you run the testcases.

The Zeniq Smartchain does net support this opcode yet, but just the instruction set defined in the 'London' version. Since Solidity 0.8.20 upwards uses the new opcode, any contract compiled with that particular or newer compiler version will result in bytecode which you can deploy to the Zeniq Smartchain, but not necessary be able to call functions from.

The following is accomplished by the stated configuration:

  1. At first, a js object holding information about the account is created. This object must contain all information necessary for hardhat to create accounts to deploy smart contracts. Normally it is enough to provide the mnemonic of your wallet plus the amount of accounts which should be inferred.
  2. Next, an object is exported from the module. It first has to provide information about how your smart contracts should be compiled. By setting the optimizer to be enabled, hardhat attempts to make your smart contract consume less gas in execution. The number of runs specifies how often the optimization process should run. In the second part, the networks are defined.

Writing the Distributors Contract

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;

contract Distributor {
function distribute(address payable[] memory targets)
payable
external {
uint length = targets.length;

require(length > 0);
require(msg.value > length, "Amount is too low");

uint amount = msg.value / length;
for(uint i = 0; i < length; i++) {
(bool success, ) = targets[i].call{value: amount}("");
require(success);
}
}
}

As you can see the contract consists only of one function called distribute. It is declared as external, meaning it can be called from outside the contract. It takes an array of address payable and is marked as payable to allow the contract to receive ether along with the function call.

The require statements make sure that the contract is called with correct parameters. For instance it is not allowed to call the contract with an empty array of addresses. One important thing to notice is that functions are atomic in solidity. Either is the whole function executed or the call is aborted, ideally with an error message. The last part of the function loops through the passed addresses and distributes ether equally. If only one distribution fails the whole call is aborted, as stated earlier.

Testing

You can launch unit tests with Hardhat. To do this, you need to write some tests in the test directory. Use 0_basic.js as the test file:

describe("Basic Distributor Tests", function () {
it('should deploy the contract', async function () {
const {accounts, distributor} = await loadFixture(deploy);
expect(distributor.address).not.null;
});

it('should distribute', async function () {
const {accounts, distributor} = await loadFixture(deploy);

let balance0 = await ethers.provider.getBalance(accounts[0]);
let balance1 = await ethers.provider.getBalance(accounts[1]);
let balance2 = await ethers.provider.getBalance(accounts[2]);

const tx = await distributor.distribute([accounts[1], accounts[2]], {
value: balance0.toBigInt() / 2n
});

const result = await tx.wait();
});

Compiling and Deployment

You can compile your contracts by typing npx hardhat compile which generates a new directory containing your compiled contracts. To deploy (or migrate) the smart contract to the blockchain, one can create a deploy script:

const hre = require('hardhat');
const {ethers} = hre;

async function deploy() {
const accounts = await hre.accounts;
const contract = await ethers.deployContract('Distributor', []);
console.log('Deployed at:', contract.address);
return {
accounts,
distributor: contract
};
}

Interacting with the contract

After the contract is deployed one can interact with deployed instance of the contract. As our contract only consists of one function we can only interact with it by calling said function. Again, one can write a script for that:

const hre = require('hardhat');
const {ethers} = hre;

async function distribute() {
const contract = await ethers.getContractAt('Distributor', '0xa32eA318782533032bcf68c7E2378BecdF609060');

const accounts = [
"0x06562f53f6059D7cBDE83BC286fEc364a901DC0b",
"0x0A2944f4E3360C1B4872E0635c03b7D67e309243",
"0x566d65116a24ce5dF9C8ABd271588ebc662688d0",
"0xCa63c9FDA0019D7163bdb57a3D4a03961F0aE676",
"0x53f83E9fC0c4e2CD8f046cA3Aa9176fc5e29e892"
];

console.log('Distribute');
await contract.distribute(accounts, {
value: ethers.constants.WeiPerEther.toBigInt() / 50n
});
}

The function which can be seen above is only an example with hard coded addresses to showcase what the contract is capable of and how to interact it. In an ideal case one would create a user interface for it to enhance the user experience of whoever wants to use the contract.

For all details about the smart contract, consider exploring the official repository.