'safeTransfer' in Crowdsale errors but works with 'transfer'

I have created a simple ERC20 fixed supply token Crowdsale by extending the latest Openzeppelin Crowdsale.sol. Everything works fine… the only problem is, I struggled majorly with a vague error when buying tokens. After much debugging, I narrowed down my mysterious error to one line in the Crowdsale.sol file, that being the _token.safeTransfer line inside the _deliverTokens function. Replacing this with _token.transfer instead works. But this is obviously not ideal. I have entirely failed, however, to figure out why this is the case.

:computer: Environment
Ubuntu 20.04 – Firefox browser with Metamask
Using Ganache as my test network - v2.5.4 (I import my ganache accounts into Metamask)
Node - v14.15.4
Truffle - v5.1.65
solc - I have tested this with versions - 0.6.2, 0.7.6, 0.8.0, 0.8.1 all with the same results.
Openzeppelin/contracts - v3.4.0 & v – I have tested with both the newest contracts from the github repo, aswell as the current version available from npm, which looks to be not as new(well the pragma solidity lines are different at least).

*Since Crowdsale.sol is no longer in the main repo, I am using the last version available - v2.5.1

:memo:Details
So, I got my project to work as desired by doing the following:

function _deliverTokens(address beneficiary, uint256 tokenAmount) internal virtual {
//        _token.safeTransfer(beneficiary, tokenAmount);
    _token.transfer(beneficiary, tokenAmount);
}

As you can see, using the transfer method on the token works fine, but using safeTransfer gives me the following error, which is taken from the web console in my browser (as you can see it’s an error from Metamask):

MetaMask - RPC Error: Error: [ethjs-query] while formatting outputs from RPC '{"value":{"code":-32603,"data":{"message":"VM Exception while processing transaction: invalid opcode","code":-32000,"data":{"0xafb40518d0061e1e9ef0c3afee0da626b54fe84fdc89539acff921f468c60747":{"error":"invalid opcode","program_counter":1857,"return":"0x"},"stack":"RuntimeError: VM Exception while processing transaction: invalid opcode\n at Function.RuntimeError.fromResults (/tmp/.mount_ganachwap6ZE/resources/static/node/node_modules/ganache-core/lib/utils/runtimeerror.js:94:13)\n at BlockchainDouble.processBlock (/tmp/.mount_ganachwap6ZE/resources/static/node/node_modules/ganache-core/lib/blockchain_double.js:627:24)\n at runMicrotasks (<anonymous>)\n at processTicksAndRejections (internal/process/task_queues.js:93:5)","name":"RuntimeError"}}}}'

Notes:

  • Same error is produced if I try the transaction via web3 in the truffle console.
  • My tests which are conducted via truffle test, always pass, so this may have something to do with Ganache?

:1234: Code to reproduce

MyToken.sol

pragma solidity >=0.6.0 <=0.8.0;

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

contract MyToken is ERC20 {

    constructor(uint256 initialSupply) public ERC20("MyToken", "MINE") {
        _mint(msg.sender, initialSupply);
    }

    function decimals() public view override returns (uint8) {
        return 0;
    }
}

MyTokenSale.sol

pragma solidity >=0.6.0 <=0.8.0;

import "./Crowdsale.sol";

contract MyTokenSale is Crowdsale {

constructor(
    uint256 rate,    // rate in TKNbits
    address payable wallet,
    IERC20 token
)
Crowdsale(rate, wallet, token)
public
{
}

Crowdsale.sol – The change I made is in the _deliverTokens function

// SPDX-License-Identifier: TEMP
pragma solidity >=0.6.0 <=0.8.0;

import "@openzeppelin/contracts/GSN/Context.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/math/SafeMath.sol";
import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

/**
 * @title Crowdsale
 * @dev Crowdsale is a base contract for managing a token crowdsale,
 * allowing investors to purchase tokens with ether. This contract implements
 * such functionality in its most fundamental form and can be extended to provide additional
 * functionality and/or custom behavior.
 * The external interface represents the basic interface for purchasing tokens, and conforms
 * the base architecture for crowdsales. It is *not* intended to be modified / overridden.
 * The internal interface conforms the extensible and modifiable surface of crowdsales. Override
 * the methods to add functionality. Consider using 'super' where appropriate to concatenate
 * behavior.
 */
contract Crowdsale is Context, ReentrancyGuard {
    using SafeMath for uint256;
    using SafeERC20 for IERC20;

    // The token being sold
    IERC20 private _token;

    // Address where funds are collected
    address payable private _wallet;

    // How many token units a buyer gets per wei.
    // The rate is the conversion between wei and the smallest and indivisible token unit.
    // So, if you are using a rate of 1 with a ERC20Detailed token with 3 decimals called TOK
    // 1 wei will give you 1 unit, or 0.001 TOK.
    uint256 private _rate;

    // Amount of wei raised
    uint256 private _weiRaised;

    /**
     * Event for token purchase logging
     * @param purchaser who paid for the tokens
     * @param beneficiary who got the tokens
     * @param value weis paid for purchase
     * @param amount amount of tokens purchased
     */
    event TokensPurchased(address indexed purchaser, address indexed beneficiary, uint256 value, uint256 amount);

    /**
     * @param rate Number of token units a buyer gets per wei
     * @dev The rate is the conversion between wei and the smallest and indivisible
     * token unit. So, if you are using a rate of 1 with a ERC20Detailed token
     * with 3 decimals called TOK, 1 wei will give you 1 unit, or 0.001 TOK.
     * @param wallet Address where collected funds will be forwarded to
     * @param token Address of the token being sold
     */
    constructor (uint256 rate, address payable wallet, IERC20 token) public {
        require(rate > 0, "Crowdsale: rate is 0");
        require(wallet != address(0), "Crowdsale: wallet is the zero address");
        require(address(token) != address(0), "Crowdsale: token is the zero address");

        _rate = rate;
        _wallet = wallet;
        _token = token;
    }

    /**
     * @dev fallback function ***DO NOT OVERRIDE***
     * Note that other contracts will transfer funds with a base gas stipend
     * of 2300, which is not enough to call buyTokens. Consider calling
     * buyTokens directly when purchasing tokens from a contract.
     */
    receive () external payable {
        buyTokens(_msgSender());
    }

    /**
     * @return the token being sold.
     */
    function token() public view returns (IERC20) {
        return _token;
    }

    /**
     * @return the address where funds are collected.
     */
    function wallet() public view returns (address payable) {
        return _wallet;
    }

    /**
     * @return the number of token units a buyer gets per wei.
     */
    function rate() public view returns (uint256) {
        return _rate;
    }

    /**
     * @return the amount of wei raised.
     */
    function weiRaised() public view returns (uint256) {
        return _weiRaised;
    }

    /**
     * @dev low level token purchase ***DO NOT OVERRIDE***
     * This function has a non-reentrancy guard, so it shouldn't be called by
     * another `nonReentrant` function.
     * @param beneficiary Recipient of the token purchase
     */

    function buyTokens(address beneficiary) public nonReentrant payable {

        uint256 weiAmount = msg.value;
        _preValidatePurchase(beneficiary, weiAmount);

        // calculate token amount to be created
        uint256 tokens = _getTokenAmount(weiAmount);

        // update state
        _weiRaised = _weiRaised.add(weiAmount);

        _processPurchase(beneficiary, tokens);
        emit TokensPurchased(_msgSender(), beneficiary, weiAmount, tokens);

        _updatePurchasingState(beneficiary, weiAmount);

        _forwardFunds();
        _postValidatePurchase(beneficiary, weiAmount);
    }

    /**
     * @dev Validation of an incoming purchase. Use require statements to revert state when conditions are not met.
     * Use `super` in contracts that inherit from Crowdsale to extend their validations.
     * Example from CappedCrowdsale.sol's _preValidatePurchase method:
     *     super._preValidatePurchase(beneficiary, weiAmount);
     *     require(weiRaised().add(weiAmount) <= cap);
     * @param beneficiary Address performing the token purchase
     * @param weiAmount Value in wei involved in the purchase
     */
    function _preValidatePurchase(address beneficiary, uint256 weiAmount) internal view virtual {
        require(beneficiary != address(0), "Crowdsale: beneficiary is the zero address");
        require(weiAmount != 0, "Crowdsale: weiAmount is 0");
        this; // silence state mutability warning without generating bytecode - see https://github.com/ethereum/solidity/issues/2691
    }

    /**
     * @dev Validation of an executed purchase. Observe state and use revert statements to undo rollback when valid
     * conditions are not met.
     * @param beneficiary Address performing the token purchase
     * @param weiAmount Value in wei involved in the purchase
     */
    function _postValidatePurchase(address beneficiary, uint256 weiAmount) internal view virtual {
        // solhint-disable-previous-line no-empty-blocks
    }

    /**
     * @dev Source of tokens. Override this method to modify the way in which the crowdsale ultimately gets and sends
     * its tokens.
     * @param beneficiary Address performing the token purchase
     * @param tokenAmount Number of tokens to be emitted
     */
    function _deliverTokens(address beneficiary, uint256 tokenAmount) internal virtual {
        _token.safeTransfer(beneficiary, tokenAmount);
//        _token.transfer(beneficiary, tokenAmount);
    }

    /**
     * @dev Executed when a purchase has been validated and is ready to be executed. Doesn't necessarily emit/send
     * tokens.
     * @param beneficiary Address receiving the tokens
     * @param tokenAmount Number of tokens to be purchased
     */
    function _processPurchase(address beneficiary, uint256 tokenAmount) internal virtual {
        _deliverTokens(beneficiary, tokenAmount);
    }

    /**
     * @dev Override for extensions that require an internal state to check for validity (current user contributions,
     * etc.)
     * @param beneficiary Address receiving the tokens
     * @param weiAmount Value in wei involved in the purchase
     */
    function _updatePurchasingState(address beneficiary, uint256 weiAmount) internal virtual {
        // solhint-disable-previous-line no-empty-blocks
    }

    /**
     * @dev Override to extend the way in which ether is converted to tokens.
     * @param weiAmount Value in wei to be converted into tokens
     * @return Number of tokens that can be purchased with the specified _weiAmount
     */
    function _getTokenAmount(uint256 weiAmount) internal view virtual returns (uint256) {
        return weiAmount.mul(_rate);
    }

    /**
     * @dev Determines how ETH is stored/forwarded on purchases.
     */
    function _forwardFunds() internal {
        _wallet.transfer(msg.value);
    }
}
1 Like

Hi @Crisco,

Welcome to the community :wave:

As you are looking at a crowdsale, I suggest going through: Points to consider when creating a fungible token (ERC20, ERC777)

Note: Crowdsales were removed in OpenZeppelin Contracts 3.x. They are only found in OpenZeppelin Contracts 2.x, the latest version is 2.5.1 and uses Solidity 0.5: https://docs.openzeppelin.com/contracts/2.x/crowdsales

One option is to use OpenZeppelin Contracts 2.5.1 Crowdsale as is and Solidity 0.5, see: Simple ERC20 Crowdsale. I would suggest looking at this to start to make sure that you have everything working as required.

I am not sure why you were having an issue with safeTransfer. Given that you are using your own token and can check it is ERC20 compliant, then you don’t need to use safeTransfer.