Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Airdrop utility based on the disperse protocol #680

Merged
merged 10 commits into from
May 3, 2024
Merged
43 changes: 43 additions & 0 deletions products/airdrop/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Airdrop Utility

Check failure on line 1 in products/airdrop/README.md

View workflow job for this annotation

GitHub Actions / Trunk Check

prettier

Incorrect formatting, autoformat by running 'trunk fmt'

The ```Disperse``` contract and script in this repo provide a handy utility to automate batched transfers to a large number of accounts. It's useful for airdrops and other events where hundreds or tousands of accounts have to be sent an individual amount of a token. The contract supports the native token as well as ```ERC20``` tokens, but the script can currently handle only ```ERC20``` tokens.

The contract in ```contracts/Disperse.sol``` is an updated version of the original contract implemented by the [Disperse Protocol](https://disperse.app/disperse.pdf) ported to Solidity 0.8.20. The original contract is deployed and actively used on the Ethereum mainnet: https://etherscan.io/address/0xD152f549545093347A162Dce210e7293f1452150.

Check notice on line 5 in products/airdrop/README.md

View workflow job for this annotation

GitHub Actions / Trunk Check

markdownlint(MD034)

[new] Bare URL used

## Installation

Install [Hardhat](https://hardhat.org/hardhat-runner/docs/getting-started#installation) and configure your network(s) and account(s) in ```hardhat.config.js```.

There are already existing deployments on the following networks:

| Network | ```Disperse``` contract address
| -- | --
| Zilliqa 1 mainnet | ```0x8Cc17F9eA46cD1A98EbB1Dd5495067e3095956aA```
| Zilliqa 1 testnet | ```0x38048F4B71a87a31d21C86FF373a91d1E401bea5```

Deploy a new ```Disperse``` contract only if it does not yet exist on the respective network:
```

Check notice on line 19 in products/airdrop/README.md

View workflow job for this annotation

GitHub Actions / Trunk Check

markdownlint(MD031)

[new] Fenced code blocks should be surrounded by blank lines

Check notice on line 19 in products/airdrop/README.md

View workflow job for this annotation

GitHub Actions / Trunk Check

markdownlint(MD040)

[new] Fenced code blocks should have a language specified
npx hardhat run scripts/deploy.js --network <one_of_the_networks_in_hardhat.config.js>
```

The script will output the address of the ```Disperse``` contract you can add to the table above and use in the ```disperse.js``` script below.

## Usage

Adjust the ```disperse.js``` script as follows:
* change the address in line 9 to the address at which the ```Disperse``` contract is deployed on the respective network,

Check notice on line 28 in products/airdrop/README.md

View workflow job for this annotation

GitHub Actions / Trunk Check

markdownlint(MD032)

[new] Lists should be surrounded by blank lines
* change the address in line 10 to the address at which your ```ERC20``` token is deployed on the respective network,
* change how many accounts to send tokens to per ```batch``` in line 11

Prepare the ```input.csv``` file. Each line consists of two values separated by a comma:
* the value before the comma is a hex or bech32 formatted address

Check notice on line 33 in products/airdrop/README.md

View workflow job for this annotation

GitHub Actions / Trunk Check

markdownlint(MD032)

[new] Lists should be surrounded by blank lines
* the value after the comma is an integer amount

Note that the script does not support decimal numbers as amount.

Check notice on line 36 in products/airdrop/README.md

View workflow job for this annotation

GitHub Actions / Trunk Check

markdownlint(MD009)

[new] Trailing spaces

Transfer the total amount of tokens to be distributed to the account that will be used to execute the script as configured in ```hardhat.config.js```.

Run the script:
```

Check notice on line 41 in products/airdrop/README.md

View workflow job for this annotation

GitHub Actions / Trunk Check

markdownlint(MD031)

[new] Fenced code blocks should be surrounded by blank lines

Check notice on line 41 in products/airdrop/README.md

View workflow job for this annotation

GitHub Actions / Trunk Check

markdownlint(MD040)

[new] Fenced code blocks should have a language specified
npx hardhat run scripts/disperse.js --network <one_of_the_networks_in_hardhat.config.js>
```
31 changes: 31 additions & 0 deletions products/airdrop/contracts/Disperse.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

contract Disperse {

function disperseEther(address payable[] memory recipients, uint256[] memory values) external payable {
for (uint256 i = 0; i < recipients.length; i++)
recipients[i].transfer(values[i]);
uint256 balance = address(this).balance;
if (balance > 0)
payable(msg.sender).transfer(balance);
}

function disperseToken(IERC20 token, address[] memory recipients, uint256[] memory values) external {
uint256 total = 0;
for (uint256 i = 0; i < recipients.length; i++)
total += values[i];
require(token.transferFrom(msg.sender, address(this), total));
for (uint256 i = 0; i < recipients.length; i++)
require(token.transfer(recipients[i], values[i]));
}

function disperseTokenSimple(IERC20 token, address[] memory recipients, uint256[] memory values) external {
for (uint256 i = 0; i < recipients.length; i++)
require(token.transferFrom(msg.sender, recipients[i], values[i]));
}
}

17 changes: 17 additions & 0 deletions products/airdrop/scripts/deploy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const hre = require("hardhat");

Check failure on line 1 in products/airdrop/scripts/deploy.js

View workflow job for this annotation

GitHub Actions / Trunk Check

prettier

Incorrect formatting, autoformat by running 'trunk fmt'

async function main() {

const Disperse = await ethers.getContractFactory("Disperse");
const disperse = await Disperse.deploy();
await disperse.waitForDeployment();
console.log(disperse.target);

}

main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
47 changes: 47 additions & 0 deletions products/airdrop/scripts/disperse.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const hre = require("hardhat");

Check failure on line 1 in products/airdrop/scripts/disperse.js

View workflow job for this annotation

GitHub Actions / Trunk Check

prettier

Incorrect formatting, autoformat by running 'trunk fmt'

const { fromBech32Address, toBech32Address } = require("@zilliqa-js/crypto");
const { validation } = require("@zilliqa-js/util");
const { open } = require("node:fs/promises");

async function main() {

const disperse = await ethers.getContractAt("Disperse", "0x38048F4B71a87a31d21C86FF373a91d1E401bea5");
const token = await ethers.getContractAt("ERC20", "0xf01f7FF8E38759707eE4167f0db48694677D15ad");
const batch = 100;

const decimals = await token.decimals();
const multiplier = BigInt(10) ** decimals;

const recipients = [];
const amounts = [];
var total = BigInt(0);
const file = await open("./scripts/input.csv");
for await (const line of file.readLines()) {
const [address, amountStr] = line.split(",");
const recipient = address && validation.isBech32(address) ? fromBech32Address(address) : address;
recipients.push(recipient.toLowerCase());
const amount = BigInt(amountStr);
amounts.push(amount * multiplier);
total += amount * multiplier;
//console.log(recipient, amount);
}

txn = await token.approve(disperse, total);
rcpt = await txn.wait();

for (start = 0; start < recipients.length; start += batch) {
end = start + batch < recipients.length ? start + batch : recipients.length;
txn = await disperse.disperseToken(token, recipients.slice(start, end), amounts.slice(start, end));
rcpt = await txn.wait();
console.log(txn.hash, start, end);
}

}

main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
2 changes: 2 additions & 0 deletions products/airdrop/scripts/input.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
zil1zh7ry007t4wul0hdcfwwe06h7emxxnthszxznw,100
0x15fc323DFE5D5DCfbeEdc25CEcbf57f676634d77,200
Loading