Airdrops tokens based on pre computed data. the data would then be computed in a Merkle Root.
The merkle root is the source of data validity, the frontend must have the .json file to make sure that the account matches and the amount also matches. and the verification when claiming will work. otherwise it will fail since the node does not exist.
sample:
{
"account": "public address",
"amount: 100
}
the amount must already be multplied by the decimal points for 6 mint decimals it is 1_000_000.
Please test the code througouhly before running.
Any loss of funds is your own responsibility as this is a one day project done without audits.
all the data needed is in the app folder
- prepare list of accounts with amounts like in sample above. or like in airdrop-data.json
- create a balance tree and merkle root
const amountsByRecipient = [];
let totalAmount = 0;
// expects to be a file of json with object [{ "account": publicKey, "amount": amount}]
for (const line of airdropData) {
const { account, amount } = line;
totalAmount += Number(amount);
amountsByRecipient.push({
account: new PublicKey(account),
// the amount must be multiplied by decimal points
amount: new anchor.BN(Number(amount)),
});
}
// balance tree of the airdrop data
const tree = new BalanceTree(amountsByRecipient);
// merkle root tree
const merkleRoot = tree.getRoot();
- run initIxn, the second argument in init is if the token is a token2022 or not
const initIx = await merkleAirdropProgram.methods
.init(toBytes32Array(merkleRoot), false)
.accounts({
authority: provider.publicKey,
tokenMint,
airdropState,
splTokenProgram: TOKEN_PROGRAM_ID,
systemProgram: SystemProgram.programId,
})
.instruction();
- send tokens to Airdrop State account
- users in the airdrop-data.json can now claim their tokens. the claim instruction creates an ata if it doesn't exist. the amount + index must be computed from the airdrop-data.json
// index is the index of the account/publickey in the file
const index = 0;
// merkle proof
const proofStrings: Buffer[] = tree.getProof(
index,
amountsByRecipient[index].account,
amountsByRecipient[index].amount
);
const proofBytes: number[][] = proofStrings.map((p) => toBytes32Array(p));
let verificationData = Buffer.allocUnsafe(8);
verificationData.writeBigUInt64LE(BigInt(index));
const testAccount = userPublicKey;
// the receipt must be here since it is only the first 8 bytes rather than the complete data
const [receipt, _receiptBump] = anchor.web3.PublicKey.findProgramAddressSync(
[Buffer.from("receipt"), testAccount.toBuffer(), verificationData],
merkleAirdropProgram.programId
);
for (const proofElem of proofBytes) {
verificationData = Buffer.concat([
verificationData,
Buffer.from(proofElem),
]);
}
// last transaction: Claim instruction
const claimIxn = await merkleAirdropProgram.methods
.claim(
toBytes32Array(merkleRoot),
amountsByRecipient[index].amount,
verificationData
)
.accounts({
owner: testAccount,
ownerMintAta: associatedAddress({
mint: tokenMint,
owner: testAccount,
}),
tokenMint,
receipt,
airdropState,
vault,
splTokenProgram: TOKEN_PROGRAM_ID,
ataProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
})
.signers([claimorTestKeypair])
.instruction();
- claimors will not be able to claim again because we create a reciept based on their data.