Pablo Ruiz

@pabloruiz55

How To Script An Automatic Token Airdrop for 40k subscribers

Photo by Dose Media on Unsplash

I’m happy to announce that I recently joined the Polymath team as a Solidity Engineer to spearhead the development of a new standard for blockchain-based securities tokens. 🎆🎆🎆

In this article/tutorial I’m going to go over the process of writing a node.js script that performs an automatic token distribution/airdrop to a list of Ethereum addresses. I’m going to use the code we wrote for the Polymath Token Distribution process — which is a pretty standard ERC20 token — and go over the script I built to handle the automatic distribution of the tokens.

Originally, I was planning on running this script through Infura so I didn’t have to run a full-node locally. That required signing the transactions offline, which I was doing by using a couple of handy functions from the latest version of web3. Unfortunately, even though that worked like a charm on testrpc and on Ropsten, on Mainnet it was a disaster. Transactions were not getting picked up, it was extremely slow, costly and unreliable. 
If you want to check it out anyways, you can refer to this early commit.

The Token and Token Distribution contracts

One of my first tasks at Polymath was helping the team iron out the Token and Token Distribution smart contracts that we will use in the upcoming days to launch the POLY Token and perform the launch airdrop to the 40k of the people that subscribed to the platform.

I’m not going to go into much detail about the code of these contracts, but you can take a look at them as they have been made public on Polymath’s Github Repository.

Here’s a few things that are worth mentioning about the PolyToken.sol and PolyDistribution.sol smart contracts, that will help make sense of the rest of the tutorial:

  • PolyToken.sol is the contract for the POLY token. It’s a pretty standard ERC20 token contract with a fixed supply.
  • PolyDistribution.sol is the contract that will handle the initial distribution of the tokens. We separated the allocation of tokens for presale investors, advisors, founders, etc. from the airdrop as the process was meant to be very different. In our case, we are going to use 10 million tokens (from the 1 billion being issued) for the airdrop, giving away 250 tokens to 40,000 people. The most important function to look at for this tutorial is airdropTokens(), let’s review it:
function airdropTokens(address[] _recipient) public onlyOwnerOrAdmin {
require(now >= startTime);
uint airdropped;
for(uint i = 0; i< _recipient.length; i++)
{
if (!airdrops[_recipient[i]]) {
airdrops[_recipient[i]] = true;
require(POLY.transfer(_recipient[i], 250 * decimalFactor));
airdropped = airdropped.add(250 * decimalFactor);
}
}
AVAILABLE_AIRDROP_SUPPLY = AVAILABLE_AIRDROP_SUPPLY.sub(airdropped);
AVAILABLE_TOTAL_SUPPLY = AVAILABLE_TOTAL_SUPPLY.sub(airdropped);
grandTotalClaimed = grandTotalClaimed.add(airdropped);
}

What airdropTokens() basically does is to distribute (calling ERC20’s transfer() function) 250 POLY tokens — which are regular ERC20 tokens — to an array of addresses. For each address we receive, we transfer 250 POLY to them as long as they haven’t already received their allocation already. After the process finishes, we update the available supply and keep track of how many tokens have already been distributed.

For this tutorial we’ll only focus on distributing the tokens meant for airdrop recipients. As can be observed above, these tokens are allocated and transferred with no vesting or cliff periods. The case is not the same for the other type of allocations, those have some special conditions that need to be met before they can be transferred/sold.

If you are interested in seeing how the rest of the allocations are done, you can review the setAllocation() and transferTokens() functions from PolyDistribution.sol .

The Token Distribution Event

When the token distribution date arrives, what our team will need to do is to distribute the tokens to each account that signed up for the airdrop. That data has been collected over the past few months from the comapny’s site and it contains the addresses of every account that signed up for the airdrop and that was successfully verified during the KYC verification process. The collected data we need for the airdrop process is stored in a CSV file that has just 1 column: The Ethereum address of each subscriber.

Note that the script could be easily modified to contain not only the address of the subscriber but also how many tokens they should be transferred. In this case, since we decided to give 250 POLY to everyone, this was not necessary and we opted to hardcode that number in the distribution smart contract.

Doing an airdrop, in theory, is quite simple. All we would need to do is to call the transfer() function of the ERC20 token for each address we collected.

The above could be done by manually executing the transfer() function if we had just a handful of subscribers, but with potentially thousands of people that will want to get a hold of their tokens the moment it’s launched, doing the above, one by one, would be very time consuming.

Automating the Token Distribution Process Through a Node.js Script.

Having explained how the token and distribution contracts work, let’s dive into the JS code. We need to do a few things in order to automate the token distribution process:

  1. We have to read the CSV file and process it to remove blank or invalid entries. We assume some of the data will be missing or some addresses might be erroneous, so we’ll make sure to take those out before we send them to the blockchain.
  2. We’ll pack the addresses in multiple arrays that contain 80 addresses each. Why 80? After several tests, that was the ideal number given the gas cost of transferring tokens. Depending on what you are trying to do with each entry, it might cost more or less gas per transaction, and you should pack entries accordingly so the transaction doesn’t run out of gas and rolls back.
  3. Once we have our set of arrays we’ll pass each of them to the airdropTokens() function on the smart contract which will loop through the array and call the transfer() method for each subscriber to send them their tokens.
  4. Afterwards, we’ll run another process to get all the Transfer events generated by the distribution contract so we can review that the distributions went well. (We’ll sum the tokens distributed, which should match the data we had on file).

Let’s start by setting the project up:

If you want to skip the whole tutorial and just run the script, you can find the full source code here.

Set up

Run the commands below to set up a brand new project and install the required dependencies:

$ mkdir distributionTutorial
$ npm init
$ truffle init
$ npm install web3 fast-csv truffle-contract ethereumjs-testrpc --save

For this project we will be using a few libraries and frameworks:

  • Truffle: Which allows us to easily compile, migrate and interact with our contracts from JavaScript.
  • Fast-csv: To read and process the data form a CSV file.

You should also install Parity and sync it on Ropsten (or whichever testnet / mainnet you prefer). The following command has worked for me pretty well:

parity — chain ropsten — rpcapi “eth,net,web3,personal,parity” — unlock <THE ACCOUNT YOU WANT TO UNLOCK> — password $HOME/password.file

Next, copy the Polymath Distribution smart contracts to the contracts folder of your project. The files can be found here: https://github.com/PolymathNetwork/polymath-token-distribution/tree/master/contracts

Open truffle.js and replace its content with the following code:

module.exports = {
networks: {
development: {
host: 'localhost',
port: 8545,
network_id: '*', // Match any network id
gas: 3500000,
},
ropsten: {
host: 'localhost',
port: 8545,
network_id: '3', // Match any network id
gas: 3500000,
gasPrice: 50000000000
},
},
solc: {
optimizer: {
enabled: true,
runs: 200,
},
},
};

The above will allow us to run truffle migrate --network ropsten to deploy the contracts to Ropsten testnet. Before being able to deploy the contracts to Ropsten we need to create the deployment script for truffle. Create a new file named 2_deploy_contracts.js inside the migrations folder with the following code:

var PolyToken = artifacts.require('./PolyToken.sol');
var PolyDistribution = artifacts.require('./PolyDistribution.sol');
module.exports = async (deployer, network) => {
let _now = Date.now();
let _fromNow = 60 * 5 * 1000; // Start distribution in 1 hour
let _startTime = (_now + _fromNow) / 1000;
await deployer.deploy(PolyDistribution, _startTime);
console.log(`
---------------------------------------------------------------
--------- POLYMATH (POLY) TOKEN SUCCESSFULLY DEPLOYED ---------
---------------------------------------------------------------
- Contract address: ${PolyDistribution.address}
- Distribution starts in: ${_fromNow/1000/60} minutes
- Local Time: ${new Date(_now + _fromNow)}
---------------------------------------------------------------
`);
};

The code above will be run when you execute truffle migrate --network ropsten . It will deploy the PolyDistribution contract to Ropsten (which also handles the deployment of the POLY Token contract), setting the _startTime to five minutes from now. Make sure the_startTime variable is correctly set and that you attempt to do the airdrop once the _startTime has been reached, else the execution will fail. We are using _startTime to prevent people from withdrawing tokens before the token distribution event begins.

Go ahead and run truffle migrate --network ropsten if everything went well you should see an output similar to this on the console:

The tx hashes and contract address will be different for you.

If you don’t see this output or you get an error make sure you are running Parity and that is fully synced. Also, make sure you have enough ether in the account being used to deploy the contracts on the Ropsten testnet.

Take note of the address of the Poly Distribution contract we just deployed, we’ll use it later.

Reading the CSV File

Let’s start working on the script that will do the automatic allocation of POLY tokens to the addresses that signed up for the airdrop.

First, create a new folder called scripts and create a new file inside this folder called csv_allocation.js This file will contain all the code for running the allocation process.

Before we move forward with the code that reads the CSV file and processes it, let’s add the file to the project. We need a 1 column CSV file named airdrop.csv that has an entry for each address that will receive tokens. Create this file and add it to the scripts/data folder.

If you want to easily test the airdrop, you can generate this file yourself with “random” addresses you control. One simple way to do that is to run testrpc and specify how many accounts you want to create, like so:

testrpc -m "word1 word2 word3..." -a 300

The command above will generate 300 accounts from the mnemonic you provide it with. Copy the addresses to airdrop.csv .

Back in our csv_allocation.js script, let’s add the necessary code to be able to read the airdrop.csv. Add the following code to csv_allocation.js:

var fs = require('fs');
var csv = require('fast-csv');
var BigNumber = require('bignumber.js');
let polyDistributionAddress = process.argv.slice(2)[0];
let BATCH_SIZE = process.argv.slice(2)[1];
if(!BATCH_SIZE) BATCH_SIZE = 80;
let distribData = new Array();
let allocData = new Array();
function readFile() {
var stream = fs.createReadStream("scripts/data/airdrop.csv");
let index = 0;
let batch = 0;
console.log(`
--------------------------------------------
--------- Parsing distrib.csv file ---------
--------------------------------------------
******** Removing beneficiaries without address data
`);
var csvStream = csv()
.on("data", function(data){
let isAddress = web3.utils.isAddress(data[0]);
if(isAddress && data[0]!=null && data[0]!='' ){
allocData.push(data[0]);
index++;
if(index >= BATCH_SIZE)
{
distribData.push(allocData);
allocData = [];
index = 0;
}
}
})
.on("end", function(){
//Add last remainder batch
distribData.push(allocData);
allocData = [];
setAllocation();
});
  stream.pipe(csvStream);
}
if(polyDistributionAddress){
console.log("Processing airdrop. Batch size is",BATCH_SIZE, "accounts per transaction");
readFile();
}else{
console.log("Please run the script by providing the address of the PolyDistribution contract");
}

You can now run the script by doing:

$ node scripts/csv_allocation.js 0x0... 80
// Where 0x0... is the address of the PolyDistribution contract we previously deployed to Ropsten.
// 80 is the batch size we want to process. (How many accounts per array we want to process and send to the airdropTokens function) Can be omitted, defaults to 80.

Let’s review the code:

First, we import the libraries that will allow us to read files and process the CSV file.

Then, if you look at the last few lines of code, you will see we are accessing the parameters passed when running the script and, if there’s the address of the PolyDistribution contract, we call the readFile() function.

What the readFile() function does is accessing airdrop.csv file and it reads it line by line. At each line, we make sure that the value is not null or empty and we also use web3’s isAddress() function to verify that the address passed is valid. If the address is ok, we add it to an array that holds the processed data we’ll use to build each Ethereum transaction. 
Once the data has been fully processed and we reach the end of file, we call the function that will take each array of 80 addresses and process them.

Note that this function is very simplistic and could be further improved to detect token amounts that would exceed the POLY supply, duplicate addresses, etc. All of these cases are still taken care of on the contract side, but it would be nice if we could save us a few transaction calls to Ethereum.

Processing the Token Distribution

Now that we have our data processed into an array — We should have the array called distribData containing a handful of arrays, each with 80 addresses at most — we are going to call the airdropTokens() function from the smart contract for each one of them.

const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
async function setAllocation() {
console.log(`
--------------------------------------------
---------Performing allocations ------------
--------------------------------------------
`);
let accounts = await web3.eth.getAccounts();
let polyDistribution = await PolyDistribution.at(polyDistributionAddress);
  for(var i = 0;i< distribData.length;i++){
try{
let gPrice = 50000000000;
console.log("Attempting to allocate 250 POLYs to accounts:",distribData[i],"\n\n");
let r = await polyDistribution.airdropTokens(distribData[i],{from:accounts[0], gas:4500000, gasPrice:gPrice});
console.log("---------- ---------- ---------- ----------");
console.log("Allocation + transfer was successful.", r.receipt.gasUsed, "gas used. Spent:",r.receipt.gasUsed * gPrice,"wei");
console.log("---------- ---------- ---------- ----------\n\n")
} catch (err){
console.log("ERROR:",err);
}
 }
}

Let’s take a closer look at this function. What setAllocation() from the JS script does is simply traversing the distribData array we populated with the processed data from the csv file, then for each array of entries we proceed to execute airdropTokens() on the smart contract, passing the array.

For each batch of addresses we process, we retrieve the event log and print how much gas was spent, just as a way to make sure the process succeeded.

Every batch should spend about the same amount of gas. If there’s a batch that costed less gas, it means that some of the addresses within that batch were not transferred tokens, probably because they had already been transferred tokens before.

Reading the Transfer Event from the ERC20 Token to Verify the Transactions

One last thing we can do before calling it a day is to access the event log for the ERC20 POLY token Transfer() function so we can quickly check how many accounts got their tokens.

Add the following lines at the end of setAllocation() function:

console.log("Distribution script finished successfully.")
console.log("Waiting 2 minutes for transactions to be mined...")
await delay(90000);
console.log("Retrieving logs to inform total amount of tokens distributed so far. This may take a while...")
let polytokenAddress = await polyDistribution.POLY({from:accounts[0]});
let polyToken = await PolyToken.at(polytokenAddress);
var sumAccounts = 0;
var sumTokens = 0;
var events = await polyToken.Transfer({from: polyDistribution.address},{fromBlock: 0, toBlock: 'latest'});
events.get(function(error, log) {
event_data = log;
//console.log(log);
for (var i=0; i<event_data.length;i++){
//let tokens = event_data[i].args.value.times(10 ** -18).toString(10);
//let addressB = event_data[i].args.to;
sumTokens += event_data[i].args.value.times(10 ** -18).toNumber();
sumAccounts +=1;
//console.log(`Distributed ${tokens} POLY to address ${addressB}`);
}
console.log(`A total of ${sumTokens} POLY tokens have been distributed to ${sumAccounts} accounts so far.`);
});

The code above adds a timeout so we give some time for the transactions to finish getting mined and then we get the Transfer() event for the POLY token, filtering the events by the from field which is the PolyDistribution contract.

Then, we count the events and how many tokens were distributed. We can use that data to compare it with our original file. We could also list each address that got tokens, or add a function that compares the CSV file with the event log data, if we wanted to get fancy.

Executing the Script

That’s it! Let’s give the script a try. Run the following command:

$ node scripts/csv_allocation.js 0x0...
// Replace 0x0... with the address of the PolyDistribution contract you deployed to Ropsten

If everything went well, you should see something like this on the console:

And if you go to Etherscan and enter address of the PolyDistribution contract you deployed, you should see something like this:

If you can see the Transfer() events for each account you had on your CSV file, then congrats! 
You have successfully performed your airdrop!

Thanks for reading through this tutorial, I hope you enjoyed reading it as much as I did writing it. Stay tuned for more articles, tutorials and stories related to my adventures building the next-generation platform for tokenized securities on the blockchain!

If you are doing your own airdrop and need advice, feel free to reach out to me on the comments section below, or send me an email to me[at]pabloruiz[dot]co and I’ll do my best to reply promptly.

More by Pablo Ruiz

Topics of interest

More Related Stories