Airdrops

AirdropRecipient lets the protocol owner stitch arbitrary external contract calls into the Locker / collection lifecycle, then later distribute the resulting payouts to CollectionToken holders via merkle proofs. It covers ERC-20, ERC-721, ERC-1155, and native-ETH drops in one entry-point set.

The four entry-points form a strict pipeline:

requestAirdrop (admin)

Calls _contract with _payload, optionally forwarding _value ETH. Used to subscribe to upstream airdrops, claim points, etc.

Prerequisites

  • Caller is the AirdropRecipient owner.
  • _contract is on the owner’s allowlist (the AirdropTarget table in the subgraph mirrors this).

Inputs

function requestAirdrop(address _contract, uint _value, bytes calldata _payload)
  external payable
  returns (bool success_, bytes memory data_);

Code

requestAirdrop.ts
import { airdropRecipientAbi } from "@nftx/contracts";
import { NFTX_CONTRACTS } from "@nftx/config";
import { encodeFunctionData } from "viem";
 
const recipient = NFTX_CONTRACTS[base.id].airdropRecipient;
 
const payload = encodeFunctionData({
  abi: targetAbi,
  functionName: "claim",
  args: [merkleProof, leaf],
});
 
await walletClient.writeContract({
  address: recipient,
  abi: airdropRecipientAbi,
  functionName: "requestAirdrop",
  args: [targetContract, 0n, payload],
});

Failure modes

ErrorCause
ExternalCallFailed_contract.call(_payload) reverted

setAirdropTarget (admin)

Manages the _contract allowlist consumed by requestAirdrop.

Subgraph

AirdropTargetSet(target, allowed)AirdropTarget { allowed }.

distributeAirdrop (admin)

Publishes a merkle root and ClaimType so holders can begin claiming.

Inputs

function distributeAirdrop(bytes32 _merkle, Enums.ClaimType _claimType) external;

Code

distributeAirdrop.ts
await walletClient.writeContract({
  address: recipient,
  abi: airdropRecipientAbi,
  functionName: "distributeAirdrop",
  args: [merkleRoot, /* ClaimType.ERC20 */ 0],
});

Failure modes

ErrorCause
MerkleAlreadyExists(_merkle, _claimType) was already published
MerkleRootNotValid_merkle == bytes32(0)

Subgraph

AirdropDistributed(merkle, claimType) → new AirdropDistribution { id: chainId-merkle-claimType, createdAt }.

claimAirdrop

Holder-side entry. The merkle proof is generated off-chain (by the FE proof service) from the published distribution.

Prerequisites

  • The corresponding AirdropDistribution exists and has not yet been fully claimed for the holder’s leaf (isClaimed(merkle, leaf) == false).
  • The leaf hashes to a node included in the merkle proof.

Inputs

struct MerkleClaim {
  address recipient;
  address target;     // payout token / NFT contract
  uint tokenId;       // NFT id or 0 for fungible
  uint amount;
}
 
function claimAirdrop(
  bytes32 _merkle,
  Enums.ClaimType _claimType,
  MerkleClaim calldata _node,
  bytes32[] calldata _merkleProof
) external;

Code

claimAirdrop.ts
await walletClient.writeContract({
  address: recipient,
  abi: airdropRecipientAbi,
  functionName: "claimAirdrop",
  args: [
    merkleRoot,
    /* ClaimType.ERC20 */ 0,
    {
      recipient: account,
      target: tokenAddress,
      tokenId: 0n,
      amount: 1_000_000n,
    },
    proof,
  ],
});

Failure modes

ErrorCause
InvalidClaimNodeLeaf doesn’t match (recipient, target, tokenId, amount)
AirdropAlreadyClaimedHolder already claimed this leaf
TransferFailedThe downstream payout transfer failed (e.g. ERC-20 reverts on transfer)

Subgraph

AirdropClaimed(merkle, claimType, recipient, target, tokenId, amount)AirdropClaim { id, recipient, target, tokenId, amount, txHash, claimedAt } linked back to the AirdropDistribution.

Combined view: a recipient’s airdrops

The subgraph indexes claimed rows. To list eligible-but-unclaimed airdrops you also need the off-chain proof service to enumerate that recipient’s leaves; then filter out anything that already appears in AirdropClaim.

query AirdropsForRecipient($chainId: Int!, $recipient: String!) {
  AirdropClaim(
    where: {
      chainId: { _eq: $chainId }
      recipient: { _eq: $recipient }
    }
    order_by: { claimedAt: desc }
  ) {
    airdrop { merkle claimType }
    target
    tokenId
    amount
    txHash
    claimedAt
  }
}

Native (ETH) airdrop note

For ClaimType.NATIVE the leaf’s target is address(0); the contract forwards _node.amount of ETH from AirdropRecipient’s balance to recipient. Any failure in that low-level transfer surfaces as TransferFailed.