Struggling with Airdrop contract

Hello, I hope this doesn't get flagged for a duplicate post as I have reviewed what others have sent and am very confused about how mine isn't working the same.

I am trying to create a simple batch transfer contract that allows users to input an ERC20/721/1155 address and airdrop tokens to an array of addresses. But I keep getting errors when running a transaction on the Goerli testnet via etherscan, so I was hoping to get some answers as to why it's not working for me, but is for others.

For now, I'll share the funciton I am using for the erc20 drop:

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

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

contract AirdropContractTest {

    event AirdropCoins(address indexed from, address[] to, uint256[] amount);

    /**
    * @dev Batch transfer ERC20 tokens to a list of recipients.
    * @param token Address of the ERC20 contract.
    * @param recipients List of addresses to transfer the tokens to.
    * @param amount Amount of tokens to transfer to each recipient.
     */
    function batchTransferERC20(
        IERC20 token,
        address[] calldata recipients,
        uint256[] calldata amount
    ) external {
        uint256 recipientsLength = recipients.length; // Cache the length of the recipients
        // Sender must have enough tokens to send to recipients
        require(recipientsLength== amount.length, "Each recipient needs tokens");
        
        // Loop through all the recipients and send them the specified amount
        for (uint256 i = 0; i < recipientsLength; i++) {
            require(token.transfer(recipients[i], amount[i]));
        }

        emit AirdropCoins(msg.sender, recipients, amount);
    }
}

I am caching the length of the recipients for gas saving, am doing a loop to send each address the designated amount of tokens, and then emitting an event.

When I deploy to Goerli and check on the goerli etherscan, I input the following arguments:
Token Address - 0x182323E55C07f1afa3bD555008DDe89Dd035D4D1
Addresses to airdrop to: [0x90BadE35Da052450B01e99b38Cbb550BC3f1dD58, 0xD6D7fE937a64dE974923e2f80b44DA3B18BdCc13]
Amount - 5

Currently the wallet I am using to airdrop has 9700 of that token, so 5 should be easy to send to each address, but the error I get is: Fail with error 'ERC20: transfer amount exceeds balance'.

This doesn't make sense to me and would love help in trying to figure out what the issue is.

Well, the error-message transfer amount exceeds balance says it all.

The entity executing this transfer doesn't hold a sufficient amount of tokens.

This entity happens to be your contract, so you need to make sure that your contract holds a sufficient amount of tokens before attempting to execute that function.

You are using transfer, which sends from the current address which is the smart contract itself.
If you want to token to be transferred from the person calling your contract then you will need to use transferFrom. This does require the sender to approve your contract first.

Also it is adviced to use the safeTransfer from openzeppelin. A lot of tokens do not adhere to the erc20 standard and might return nothing or revert depending on the token.

This was meant to be a reply to you:

So if I make the switch to transferFrom, I also need to implement the approve function before hand, right?

Like:

    function batchTransferERC20(
        IERC20 token,
        address[] calldata recipients,
        uint256[] calldata amount
    ) external {
        // Cache the length of the recipients
        uint256 recipientsLength = recipients.length;
        // Loop through all the recipients and send them the specified amount
        for (uint256 i = 0; i < recipientsLength; i++) {
            require(token.approve(msg.sender, amount[i]), "Approve failed");
            require(token.transferFrom(msg.sender, recipients[i], amount[i]), "Transfer failed");
        }

        emit AirdropCoins(msg.sender, recipients, amount);
    }

I guess I'm confused on why the contract needs to hold a balance if the function being called is transfer, which is just address to address transfer?

I noticed CryptoWorld mentioned transferFrom so I was going to implement that strategy instead

No, you just need to:

  1. Change transfer(...) to transferFrom(mag.sender, ...) and redeploy your contract
  2. Execute token.approve(airdropContractAddress, sumOfAllAmounts) using your wallet
  3. Execute airdropContract.batchTransferERC20(...) using your wallet

As recommended in one of the answers above, consider using OpenZeppelin safeTransferFrom.

safeTransferFrom isn't available in the IERC20 interface for me to use unfortunately. And I don't understand 2 and 3?

Is that not something I put into the batchTransferERC20 function?

Function safeTransferFrom is not a part of the ERC20 standard, but a part of a wrapping library.
In order to learn how you can use this library, scroll to the top of the page that I've linked above.

Regarding 2 and 3 - those are the two transactions that you need to execute using your wallet (i.e., using a wallet which holds the total amount of tokens that you want to airdrop).

Okay so I would swap out the IERC20 with the library and then implement safeTransferFrom instead of transferFrom, like this:

import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

contract AidropTest {
    using SafeERC20 for IERC20;

    function batchTransferERC20(
        IERC20 token,
        address[] calldata recipients,
        uint256[] calldata amount
    ) external {
        // Cache the length of the recipients
        uint256 recipientsLength = recipients.length;
        // Loop through all the recipients and send them the specified amount
        for (uint256 i = 0; i < recipientsLength; i++) {
            token.safeTransferFrom(msg.sender, recipients[i], amount[i]);
        }

        emit AirdropCoins(msg.sender, recipients, amount);
    }
};

And then when someone wants to call the function to initiate an airdrop, they will implement the token address, the array of recipients and the amounts each person gets, correct?

I still don't understand where token.approve comes into play here. My assumption is that it's implemented before the for loop like:

token.approve(addresses, totalAmount);
// execute loop here

Is that correct?

If you follow my previous answer, then you should execute token.approve(contractAddress, totalAmount) using your wallet, not as part of the contract function or anything like that.

Alternatively to my previous answer, you can simply transfer the total amount of tokens to the contract, before you execute function batchTransferERC20, as I mentioned in my original answer at the top of this page.

Ah I see okay so the user would deposit the total amount into the contract first, and then the batchTransferERC20 will run with the tokens deposited into the contract, thus satisfying the requirements of needing the balance to exceed the amount. That makes sense, I can figure out a way to implement that.

If by "user" you mean, the one in charge of executing function batchTransferERC20, then yes.

Yes sorry by user I mean the caller of the function or msg.sender with their wallet and the token they want to disperse to others.

So in your opinion, would this be a viable option to execute this?

    function batchTransferERC20(
        IERC20 token,
        address[] calldata recipients,
        uint256[] calldata amount
    ) external {
        // Cache the length of the recipients
        uint256 recipientsLength = recipients.length;
        
        // Calculate the total amount of tokens being transferred
        uint256 sumOfAmounts = 0;
        for (uint256 i = 0; i < amount.length; i++) {
            sumOfAmounts += amount[i];
        }

        // Send tokens to contract
        token.safeTransfer(address(this), sumOfAmounts);

        // Loop through all the recipients and send them the specified amount
        for (uint256 i = 0; i < recipientsLength; i++) {
            token.safeTransferFrom(address(this), recipients[i], amount[i]);
        }

        emit AirdropCoins(msg.sender, recipients, amount);
    }
  1. The caller of the function will deposit the total amount of tokens to the contract.
  2. The contract will then execute the safeTransferFrom with the contract address as the from, each recipient as to and the amount each will get as the value
  3. Then it will emit the event showing the address that called the function, the list of recipients, and the amount sent to each

Is this a route that satisfies the requirements?

No, this is wrong.
Your original contract was fine, you just need to proceed with either one of the two options which I have suggested to you throughout this thread.

Option 1, as I've suggested in my original answer:

  1. Execute token.transfer(airdropContractAddress, sumOfAllAmounts) using your wallet
  2. Execute airdropContract.batchTransferERC20(...) using your wallet

Option 2, as I've suggested in my later answer:

  1. Change transfer(...) to transferFrom(mag.sender, ...) and redeploy your contract
  2. Execute token.approve(airdropContractAddress, sumOfAllAmounts) using your wallet
  3. Execute airdropContract.batchTransferERC20(...) using your wallet

By 'using your wallet' I mean, sign the transaction with your wallet (NOT "add code in your contract").

For option 1, I don't see how that's different than my original post, which was giving an error.

I am running token.transfer and passing in an array of addresses + the amount to each array, which is exactly what you've put down for token.transfer(airdropContractAddress, sumOfAllAmounts)

Apologies for all the confusion, but I am just not seeing how this is any different. I am now using safeTransferFrom instead of transfer as per this forum, so would this be the final result?

    function batchTransferERC20(
        IERC20 token,
        address[] calldata recipients,
        uint256[] calldata amount
    ) external {
        // Cache the length of the recipients to reduce gas
        uint256 recipientsLength = recipients.length;
        // Loop through all the recipients and send them the specified amount
        for (uint256 i = 0; i < recipientsLength; i++) {
            token.safeTransferFrom(msg.sender, recipients[i], amount[i]);
        }

        emit AirdropCoins(msg.sender, recipients, amount);
    }

By "Execute token.transfer(airdropContractAddress, sumOfAllAmounts) using your wallet", I meant that you should transfer tokens FROM YOUR WALLET to the contract.

I think maybe the confusion with this lies in how I want the contract to be used, because I thought I tried that above with the example, and was told it was wrong? I want to make it so anyone can implement a token address, a list of addresses, and the amount each address will get from that token.

I totally understand needing to deposit it into the contract and then the contract can facilitate the transaction, but I don't get how this is "wrong" if that's exactly what it's intended to do?

 function batchTransferERC20(
        IERC20 token,
        address[] calldata recipients,
        uint256[] calldata amount
    ) external {
        // Cache the length of the recipients
        uint256 recipientsLength = recipients.length;
        
        // Calculate the total amount of tokens being transferred
        uint256 sumOfAmounts = 0;
        for (uint256 i = 0; i < amount.length; i++) {
            sumOfAmounts += amount[i];
        }

        // Send tokens to contract
        token.transfer(address(this), sumOfAmounts);

        // Loop through all the recipients and send them the specified amount
        for (uint256 i = 0; i < recipientsLength; i++) {
            token.transferFrom(address(this), recipients[i], amount[i]);
        }

        emit AirdropCoins(msg.sender, recipients, amount);
    }

In this it would transfer the tokens to the contract, using the transfer with address(this) aka the contract address upon deployment, and then the total amount of tokens to send. After the deposit, it would execute the for loop and run the transferFrom function which pulls from the contract, and sends the split amounts to each recipient.

So there are 2 ways to do what you want.

1: user sends tokens to your contract (outside your contract) and then calls the batch function to distribute. Then your initial posted code is correct. And you will only have to call safeTransfer(recipient, amount)

2: user approves your contract for transfer (this is outside your contract). Then user calls the batch function to distribute. Then you can use safeTransferFrom(useraddres, recipiënt, amount)

You can first transfer the tokens from the user to your contract and then to the recipient, but then the user would still have to approve your smart contract first. So you are doing 1 unneeded transaction (this is the last code posted).

In your mind, what is the most logical solution? Because I have seemingly tried both and both have failed with errors. So I am genuinely lost on what is wrong.

Ideally I would like the user to deposit the tokens into the contract, then call the batch function to distribute. Would that require two separate functions? Like function depositTokens and then function batchTransferERC20 ?

The quickest way to get your project running:

  1. Transfer the total amount of tokens to your original contract
  2. Execute your contract function batchTransferERC20

No additional code seems to be required in your original contract.

Both steps are to be executed from the same script or test or whatever it is that you've used so far.