How to make an ERC20 Airdrop

Airdropping tokens means making lot of transfers to multiple addresses in a short period of time.
Usually we want to make those in the same transaction but that's not always possible.
Let's dive in ERC20 Airdrops.

Airdrop is a "multiple transfer to"
So we need to make transfers, calling the function `transfer` of the `ERC20` token.

We'll use the ERC20 token made [here] link to the erc20 fast guide

Let's now create our Airdrop contract image

We define two functions:

  • One to airdrop multiple address with the same amount of tokens
  • One to airdrop multiple address with different amount of tokens

As we are writing an external contract, we must use transferFrom and approve that contract BEFORE calling it.

Let's write some tests

👷‍♂️HardHat

image

Import Chai & Hardhat, then define a simple function to print out the gas spent in our transaction, in gwei & USDT.
By default we consider ETH @ 1800$ and 30GWEI of gasPrice (low-medium usage of the chain)

Let's now prepare our deployment test & fixture of the token + airdrop contract.
We just check for the .mint() on ERC20 deployment & deploy of both contracts. image

Now for the real tests.
Initialize the fixture, then let's create a list of 100 address, but those are ALL THE SAME.

You need to understand this

When you send 1 token to an address, you change the storage uint value of their balance from 0 to X, this is more expensive (in gas) then changing X to non-zero value.

Let's continue.

We approve the airdrop contract to spend 100.000 Tokens, this is needed by the transferFrom function inside the airdrop contract.
Finally we call the airdrop contract and we check both user & owner balances. image

Let's now send those tokens to unique addresses image

Let's run the tests and see the difference!

Ready??

image Whew.

Be careful with what you simulate on hardhat and in your tests...
always remember that you may have something different from the real chain!
An impersonate transfer, an ETH balance...
something may always be different because of your test code.

So... on Ethereum, with 30 GWEI of gasPrice, with 100 wallets, we spend 39$ to airdrop 100.000 Tokens.

Let's change the amount to 5 image mhm... curious, a very small difference.

Is there any better way to airdrop?
Well, maybe it's not better to read but it's indeed more efficient and less expensive.
image

We are taking a strange alien code from someone.

Can we trust this?

Yes but we need to check out who wrote this and why
You can find more info about him [here] link to @PopPunkOnChain

If you don't understand assembly no worries, you're not alone.
This is an advanced language you can use in your solidity contracts to reduce the gas usage by accessing low-level calls.
That's dangerous and should be done on mainnet only if you know at 100% what are you doing.
I don't advice anyone to write in assembly without a proper training.
You need to read dedicated articles on this.
Here we just test it.
Let's see how it goes.
image

39.25$ vs 23.80$
156.32$ vs 140.87$

O.o
Where did the money go?
To the miners of course.
Why?
Because you wrote "dirty" code.
Gas is bad and you waste it by writing normal code.
What you write in solidity get converted in bytecode by the compiler.
Please keep practicing with Solidity and use it...
just keep in mind Assembly & Huff exists, you may want to encounter those later.

We used someone else's code, let's test it in-depth.
Instead of saving a single user, let's write an expect to test each one of the generated wallets.
We tested the owner's balance and it's passing, but let's write another test.
It won't cost you nothing.
If you're lazy, just duplicate the test and add

// check every address
            for (let i = 0; i < 100; i++) {
                expect(await token.balanceOf(addressList[i])).to.equal(ethers.utils.parseEther("1000"));
            }

You spent +2 minutes of your life but at least you tested the whole "alien" function.

Similar guide is HOW TO ADD AIRDROP FUNCTION TO ERC20Token

If instead we want to implement the airdrop function directly in our contract, we simply have to add a similar function but with a call to the internal _transfer directly ![[10.png]]

🛠️ Foundry

Let's create a file called Airdrop.t.sol into /test/Erc20Airdrop and let's prepare a basic test

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

import "forge-std/Test.sol";
import { Token } from "../../contracts/Erc20/Token.sol";

contract AirdropTest is Test {
    Token token;
    uint totalSupply = 100_000;
    function setUp() public {
        // deploy ERC20 token
        token = new Token("MyToken", "MTK", totalSupply);
    }

    function test_token_deployed() public {
        assertEq(token.balanceOf(address(this)), totalSupply);
    }
}

let's run it with forge test --mc AirdropTest

forge test --mc AirdropTest
[⠒] Compiling...
[⠰] Compiling 1 files with 0.8.20
[⠔] Solc 0.8.20 finished in 1.30s
Compiler run successful!

Running 1 test for test/Erc20Airdrop/Airdrop.t.sol:AirdropTest
[FAIL. Reason: assertion failed] test_token_deployed() (gas: 24418)
Test result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 687.20µs
 
Ran 1 test suites: 0 tests passed, 1 failed, 0 skipped (1 total tests)

Failing tests:
Encountered 1 failing test in test/Erc20Airdrop/Airdrop.t.sol:AirdropTest
[FAIL. Reason: assertion failed] test_token_deployed() (gas: 24418)

Encountered a total of 1 failing tests, 0 tests succeeded

Notice how tests are failing.
That's because the totalSupply is being multiplicated by token decimals in constructor.
To fix that, simply change

assertEq(token.balanceOf(address(this)), totalSupply);

into

assertEq(token.balanceOf(address(this)), totalSupply * 10 ** token.decimals());

You can also use totalSupply*1e18 if you prefer.

Let's now test the airdrop function

function test_airdrop() public {
        uint currentBalance = token.balanceOf(address(this));
        uint accounts = 10;
        uint amountToAirdrop = 10000;
        // create a list of addresses and amounts
        address[] memory addresses = new address[](accounts);
        uint[] memory amounts = new uint[](accounts);
        
        // populate lists
        for (uint i = 0; i < accounts; i++) {
            addresses[i] = vm.addr(i+100000);
            amounts[i] = amountToAirdrop / accounts;
        }

        // make the airdrop
        token.airdrop(addresses, amounts);

        // contract should have 10000 less tokens
        assertEq(token.balanceOf(address(this)), currentBalance - amountToAirdrop);
        // each address should have 1000 tokens
        for (uint i = 0; i < accounts; i++) {
            assertEq(token.balanceOf(addresses[i]), amountToAirdrop / accounts);
        }
    }

We save the current balance of the contract, how much accounts we want to airdrop and how much tokens we want to send to each account.
We create two arrays, one for addresses and one for amounts.
We populate those arrays with a for loop.
Then we call the airdrop function.
Finally we check the balance of the contract and the balance of each address.

Let's run the test
forge test --mc AirdropTest

Running 2 tests for test/Erc20Airdrop/Airdrop.t.sol:AirdropTest
[PASS] test_airdrop() (gas: 276684)
[PASS] test_token_deployed() (gas: 11110)
Test result: ok. 2 passed; 0 failed; 0 skipped; finished in 1.32ms
 
Ran 1 test suites: 2 tests passed, 0 failed, 0 skipped (2 total tests)

Now let's prepare the script file

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

import "forge-std/Script.sol";
import { TokenAirdrop } from "../../contracts/Erc20/TokenAirdrop.sol";

// used to configure the signer account
contract Base is Script {
    uint256 deployerPrivateKey = vm.envUint("PKEY_ACC1");
    address deployerAddress = vm.addr(deployerPrivateKey);

    modifier broadcast {
        vm.startBroadcast(deployerPrivateKey);
        _;
        vm.stopBroadcast();
    }
}

// define all the tasks of the project here
contract Tasks is Base {
    TokenAirdrop token;

    function deployToken(string memory name, string memory symbol, uint _tokenToMint) public {
        token = new TokenAirdrop(
            name,
            symbol,
            _tokenToMint
        );
    }
}

contract RunScripts is Tasks {
    function run() external broadcast {
    // deploy token
        deployToken("TokenName", "TokenSymbol", 100_000);
    }
}

Here we define:

  • deployerPrivateKey as the private key of the account we want to use to deploy the contracts
  • deployerAddress as the address of the account we want to use to deploy the contracts
  • Tasks as the contract that will contain all the tasks we want to run
  • RunScripts as the contract that will run all the tasks

Let's now run the script
forge script .\scripts\Erc20\TokenAirdrop.s.sol:RunScripts

[⠒] Compiling...
[⠊] Compiling 21 files with 0.8.23
[⠰] Solc 0.8.23 finished in 4.20s
Compiler run successful!
Script ran successfully.
Gas used: 649903

If you wish to simulate on-chain transactions pass a RPC URL.