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
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
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.
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.
Let's now send those tokens to unique addresses
Let's run the tests and see the difference!
Ready??
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
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.
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.
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 contractsdeployerAddress
as the address of the account we want to use to deploy the contractsTasks
as the contract that will contain all the tasks we want to runRunScripts
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.