1

I am working since 8 days on a problem with my smart contract and / or with my Web3 App.

If I try to use the contract with ETH it works well in the moment I try to use it with an ERC20 token (USDT) it does not work and a SafeERC20 Low level failure shows up. The approve works fine, the error appears at the stage of buying.

const { request } = await presaleContract.simulate.buyToken(
        [token.address || zeroAddress, amount],
        {
          value: token.address ? 0n : amount,
        }
      );

I have already added the ERC20 token to my contract enter image description here

Here is the buyToken function of my smart contract:

function buyToken(
        address _token,
        uint256 _amount
    ) external payable saleEnabled {
        uint256 saleTokenAmt = _token != address(0)
        ? getTokenAmount(_token, _amount)
        : getTokenAmount(address(0), msg.value);
        require(
            (totalTokensSold + saleTokenAmt) < totalTokensforSale,
            "Presale: Not enough tokens to be sale"
        );

        if (_token != address(0)) {
            require(_amount > 0);
            IERC20(_token).safeTransferFrom(msg.sender, owner(), _amount);
        } else {
            payable(owner()).transfer(msg.value);
        }

        totalTokensSold += saleTokenAmt;
        BuyerTokenDetails storage buyerDetails = buyersDetails[msg.sender];
        if (!buyerDetails.exists) {
            buyers.push(msg.sender);
            buyerDetails.exists = true;
        } else if (buyerDetails.amount > 0) {
            buyerDetails.reward += getBuyerReward(msg.sender);
        }
        buyerDetails.amount += saleTokenAmt;
        buyerDetails.lastCalculatedTime = block.timestamp;
    }

Here is the web3 part:

const buyToken = async (value: string | number, token: Token) => {
    let success = false;
    let hash;

    if (!walletClient || !address) return { success, txHash: hash };

    setLoading(true);

    try {
      const amount = parseUnits(`${value}`, token.decimals);
      console.log(amount);
      console.log(token.decimals);
      if (token.address) {
        await checkAllowance(
          token,
          address,
          config.presaleContract[chain.id],
          amount
        );
      }
      
      const { request } = await presaleContract.simulate.buyToken(
        [token.address || zeroAddress, amount],
        {
          value: token.address ? 0n : amount,
        }
      );
      
      hash = await walletClient.writeContract(request);

      await publicClient.waitForTransactionReceipt({ hash });

       const purchased_amount = await presaleContract.read.getTokenAmount([
         token.address || zeroAddress,
         amount,
       ]);

       storeTransaction({
         wallet_address: address,
         purchased_amount: +format(purchased_amount),
         paid_amount: value,
         transaction_hash: hash,
         paid_with: token.symbol,
         chain: chain.id,
       });

       storeReferralTransaction({
         purchased_amount: +format(purchased_amount),
         paid: value,
         transaction_hash: hash,
         payable_token: token.symbol,
         chain: chain.id,
       });

      fetchTokenBalances();
      fetchLockedBalance();
      fetchTotalTokensSold();

      toast.success(
        `You have successfully purchased $${
          config.saleToken[chain.id].symbol
        } Tokens. Thank you!`
      );

      success = true;
    } catch (error: any) {
      toast.error(
        error?.walk?.()?.shortMessage ||
          error?.walk?.()?.message ||
          error?.message ||
          "Signing failed, please try again!"
      );
    }

    setLoading(false);

    return { success, txHash: hash };
  };

Would be nice if someone can assist me.

Thanks in advance.

EDIT 1 : Full Code:

import config, { presaleStartTime } from "../config";
import { RootState } from "../store";
import { useSelector, useDispatch } from "react-redux";
import {
  setSaleStatus,
  setTokenPrice,
  setTotalTokensforSale,
  setTotalTokensSold,
} from "../store/presale";
import { useMemo, useState } from "react";
import {
  erc20ABI,
  useAccount,
  useNetwork,
  usePublicClient,
  useWalletClient,
} from "wagmi";
import { setBalance } from "../store/wallet";
import { toast } from "react-toastify";

import {
  createPublicClient,
  formatUnits,
  getContract,
  http,
  parseUnits,
  zeroAddress,
} from "viem";
import { presaleAbi } from "../contracts/presaleABI";
import { storeReferralTransaction, storeTransaction } from "../utils/apis";
import dayjs from "dayjs";

const publicClient = createPublicClient({
  chain: config.chains[0],
  transport: http(),
  batch: { multicall: true },
});

const useWeb3Functions = () => {
  const network = useNetwork();
  const chain = useMemo(() => {
    return network.chain && !network.chain.unsupported
      ? network.chain
      : config.chains[0];
  }, [network.chain]);
  const [loading, setLoading] = useState(false);
  const tokens = useSelector((state: RootState) => state.presale.tokens);
  const dispatch = useDispatch();
  const provider = usePublicClient();
  const { data: walletClient } = useWalletClient();
  const { address } = useAccount();

  const presaleContract = useMemo(
    () =>
      getContract({
        address: config.presaleContract[chain.id],
        abi: presaleAbi,
        walletClient: walletClient || undefined,
        publicClient,
      }),
    [walletClient, chain.id]
  );

  const fetchIntialData = async () => {
    setLoading(true);

    const [saleStatus] = await Promise.all([
      presaleContract.read.saleStatus(),
      fetchTotalTokensSold(),
      fetchTokenPrices(),
    ]);

    dispatch(setSaleStatus(saleStatus));

    setLoading(false);
  };

  const fetchTotalTokensSold = async () => {
    let extraAmount = 0;
    let incrase = 0;

    const totalTokensSold = await presaleContract.read.totalTokensSold();

    try {
      const resposne = await fetch("/settings.json");
      const settings = await resposne.json();
      extraAmount = settings?.x;
      incrase = settings?.y;
      // eslint-disable-next-line no-empty
    } catch (e) {}

    const amount = +format(totalTokensSold) || 0;
    const m = dayjs().diff(dayjs.unix(presaleStartTime), "minute");

    const ext = amount + incrase * Math.floor(m / 10);
    let total = (amount < ext ? ext : amount) + extraAmount;
    total = total > config.stage.total ? config.stage.total : total;
    dispatch(setTotalTokensSold(total));
  };

  const fetchLockedBalance = async () => {
    if (!address) return;

    const { symbol, decimals } = config.saleToken[chain.id];
    const [buyersDetails, remainingRewrards] = await Promise.all([
      presaleContract.read.buyersDetails([address]),
      presaleContract.read.getBuyerReward([address]),
    ]);

    const amount = buyersDetails[0] + buyersDetails[3] + remainingRewrards;
    const balance = +formatUnits(amount, decimals);

    dispatch(setBalance({ symbol: symbol, balance }));
  };

  const fetchTokenBalances = async () => {
    if (!address) return;

    const balancses = await Promise.all(
      tokens[chain.id].map((token) => {
        if (token.address) {
          return publicClient.readContract({
            address: token.address,
            abi: erc20ABI,
            functionName: "balanceOf",
            args: [address],
          });
        } else {
          return provider.getBalance({ address });
        }
      })
    );

    tokens[chain.id].forEach((token, index) => {
      dispatch(
        setBalance({
          symbol: token.symbol,
          balance: +formatUnits(balancses[index], token.decimals),
        })
      );
    });
  };

  const fetchTokenPrices = async () => {
    const pricses = await Promise.all(
      tokens[chain.id].map((token) => {
        if (token.address) {
          return presaleContract.read.tokenPrices([token.address]);
        } else {
          return presaleContract.read.rate();
        }
      })
    );

    tokens[chain.id].forEach((token, index) => {
      dispatch(
        setTokenPrice({
          symbol: token.symbol,
          price: +formatUnits(pricses[index], token.decimals),
        })
      );
    });
  };

  const checkAllowance = async (
    token: Token,
    owner: Address,
    spender: Address,
    amount: bigint
  ) => {
    if (!token.address || !walletClient) return;

    const tokenContract = getContract({
      address: token.address,
      abi: erc20ABI,
      walletClient,
      publicClient,
    });
    const allowance = await tokenContract.read.allowance([owner, spender]);

    if (allowance < amount) {
      const hash = await tokenContract.write.approve([
        spender,
        parseUnits("9999999999999999999999999999", token.decimals),
      ]);
      await publicClient.waitForTransactionReceipt({ hash });
      toast.success("Spend approved");
    }
  };

  const buyToken = async (value: string | number, token: Token) => {
    let success = false;
    let hash;

    if (!walletClient || !address) return { success, txHash: hash };

    setLoading(true);

    try {
      const amount = parseUnits(`${value}`, token.decimals);
      console.log(amount);
      console.log(token.decimals);
      if (token.address) {
        await checkAllowance(
          token,
          address,
          config.presaleContract[chain.id],
          amount
        );
      }
      
      const { request } = await presaleContract.simulate.buyToken(
        [token.address || zeroAddress, amount],
        {
          value: token.address ? 0n : amount,
        }
      );
      
      hash = await walletClient.writeContract(request);

      await publicClient.waitForTransactionReceipt({ hash });

       const purchased_amount = await presaleContract.read.getTokenAmount([
         token.address || zeroAddress,
         amount,
       ]);

       storeTransaction({
         wallet_address: address,
         purchased_amount: +format(purchased_amount),
         paid_amount: value,
         transaction_hash: hash,
         paid_with: token.symbol,
         chain: chain.id,
       });

       storeReferralTransaction({
         purchased_amount: +format(purchased_amount),
         paid: value,
         transaction_hash: hash,
         payable_token: token.symbol,
         chain: chain.id,
       });

      fetchTokenBalances();
      fetchLockedBalance();
      fetchTotalTokensSold();

      toast.success(
        `You have successfully purchased $${
          config.saleToken[chain.id].symbol
        } Tokens. Thank you!`
      );

      success = true;
    } catch (error: any) {
      toast.error(
        error?.walk?.()?.shortMessage ||
          error?.walk?.()?.message ||
          error?.message ||
          "Signing failed, please try again!"
      );
    }

    setLoading(false);

    return { success, txHash: hash };
  };

  const unlockingTokens = async () => {
    if (!walletClient) return;

    setLoading(true);

    try {
      const { request } = await presaleContract.simulate.unlockToken();
      const hash = await walletClient.writeContract(request);
      await publicClient.waitForTransactionReceipt({ hash });

      fetchLockedBalance();

      toast.success("Tokens unlocked successfully");
    } catch (error: any) {
      toast.error(
        error?.walk?.()?.shortMessage ||
          error?.walk?.()?.message ||
          error?.message ||
          "Signing failed, please try again!"
      );
    }

    setLoading(false);
  };

  const addTokenAsset = async (token: Token) => {
    if (!token.address || !walletClient) return;
    try {
      await walletClient.watchAsset({
        method: "wallet_watchAsset",
        params: {
          type: "ERC20",
          options: {
            address: token.address,
            symbol: token.symbol,
            decimals: token.decimals ?? 18,
            image: token.image.includes("http")
              ? token.image
              : `${window.location.origin}${token.image}`,
          },
        },
      } as any);
      toast.success("Token imported to metamask successfully");
    } catch (e) {
      toast.error("Token import failed");
    }
  };

  const parse = (value: string | number) =>
    parseUnits(`${value}`, config.saleToken[chain.id].decimals);

  const format = (value: bigint) =>
    formatUnits(value, config.saleToken[chain.id].decimals);

  return {
    loading,
    parse,
    format,
    buyToken,
    addTokenAsset,
    fetchIntialData,
    unlockingTokens,
    fetchLockedBalance,
    fetchTokenBalances,
  };
};

export default useWeb3Functions;

EDIT 2 - Complete Smart Contract

contract PresaleToken is Ownable {
    using SafeERC20 for IERC20;

    struct BuyerTokenDetails {
        uint256 amount;
        bool exists;
        bool isClaimed;
        uint256 reward;
        uint lastCalculatedTime;
    }

    uint256 public multiplier; // in bps 10000 = 100%
    uint256 public rate;
    address public saleToken;
    uint public saleTokenDec;
    uint256 public totalTokensforSale;
    bool public saleStatus;
    address[] public buyers;
    uint256 public totalTokensSold;

    mapping(address => bool) public payableTokens;
    mapping(address => BuyerTokenDetails) public buyersDetails;
    mapping(address => uint256) public tokenPrices;

    modifier saleEnabled() {
        require(saleStatus == true, "Presale: is not enabled");
        _;
    }
    modifier saleStoped() {
        require(saleStatus == false, "Presale: is not stopped");
        _;
    }

    constructor() {
        saleStatus = false;
    }

    function setParams(
        address _saleToken,
        uint256 _totalTokensforSale,
        uint256 _rate,
        uint _multiplier,
        bool _saleStatus
    ) external onlyOwner {
        require(_rate != 0);
        rate = _rate;
        saleToken = _saleToken;
        saleStatus = _saleStatus;
        saleTokenDec = IERC20Metadata(saleToken).decimals();
        totalTokensforSale = _totalTokensforSale;
        multiplier = _multiplier;
        IERC20(saleToken).safeTransferFrom(
            msg.sender,
            address(this),
            totalTokensforSale
        );
    }

    function updateRewards() internal {
        for (uint256 i = 0; i < buyers.length; i++) {
            if (buyersDetails[buyers[i]].amount > 0) {
                buyersDetails[buyers[i]].reward = getBuyerReward(buyers[i]);
                buyersDetails[buyers[i]].lastCalculatedTime = block.timestamp;
            }
        }
    }

    function setMultiplier(uint256 _multiplier) public onlyOwner {
        updateRewards();
        multiplier = _multiplier;
    }

    function increaseTotalTokensforSale(
        uint256 _totalTokensforSale
    ) external onlyOwner {
        totalTokensforSale += _totalTokensforSale;
        IERC20(saleToken).safeTransferFrom(
            msg.sender,
            address(this),
            _totalTokensforSale
        );
    }

    function stopSale() external onlyOwner {
        saleStatus = false;
    }

    function resumeSale() external onlyOwner {
        saleStatus = true;
    }

    function addPayableTokens(
        address[] memory _tokens,
        uint256[] memory _prices
    ) external onlyOwner {
        require(
            _tokens.length == _prices.length,
            "Presale: tokens & prices arrays length mismatch"
        );
        for (uint256 i = 0; i < _tokens.length; i++) {
            require(_prices[i] != 0);
            payableTokens[_tokens[i]] = true;
            tokenPrices[_tokens[i]] = _prices[i];
        }
    }

    function payableTokenStatus(
        address _token,
        bool _status
    ) external onlyOwner {
        require(payableTokens[_token] != _status);
        payableTokens[_token] = _status;
    }

    function updateTokenRate(
        address[] memory _tokens,
        uint256[] memory _prices,
        uint256 _rate
    ) external onlyOwner {
        require(
            _tokens.length == _prices.length,
            "Presale: tokens & prices arrays length mismatch"
        );
        if (_rate != 0) {
            rate = _rate;
        }
        for (uint256 i = 0; i < _tokens.length; i += 1) {
            require(payableTokens[_tokens[i]] == true);
            require(_prices[i] != 0);
            tokenPrices[_tokens[i]] = _prices[i];
        }
    }

    function getTokenAmount(
        address token,
        uint256 amount
    ) public view returns (uint256) {
        uint256 amtOut;
        if (token != address(0)) {
            require(payableTokens[token] == true, "Presale: Token not allowed");
            uint256 price = tokenPrices[token];
            amtOut = (amount * (10 ** saleTokenDec)) / (price);
        } else {
            amtOut = (amount * (10 ** saleTokenDec)) / (rate);
        }
        return amtOut;
    }

    function buyToken(
        address _token,
        uint256 _amount
    ) external payable saleEnabled {
        uint256 saleTokenAmt = _token != address(0)
        ? getTokenAmount(_token, _amount)
        : getTokenAmount(address(0), msg.value);
        require(
            (totalTokensSold + saleTokenAmt) < totalTokensforSale,
            "Presale: Not enough tokens to be sale"
        );

        if (_token != address(0)) {
            require(_amount > 0);
            IERC20(_token).safeTransferFrom(msg.sender, owner(), _amount);
        } else {
            payable(owner()).transfer(msg.value);
        }

        totalTokensSold += saleTokenAmt;
        BuyerTokenDetails storage buyerDetails = buyersDetails[msg.sender];
        if (!buyerDetails.exists) {
            buyers.push(msg.sender);
            buyerDetails.exists = true;
        } else if (buyerDetails.amount > 0) {
            buyerDetails.reward += getBuyerReward(msg.sender);
        }
        buyerDetails.amount += saleTokenAmt;
        buyerDetails.lastCalculatedTime = block.timestamp;
    }

    function getBuyerReward(address buyer) public view returns (uint256) {
        if (buyersDetails[buyer].amount == 0) return 0;

        uint daysPassed = (block.timestamp -
        buyersDetails[buyer].lastCalculatedTime) / 86400;

        uint256 reward = (buyersDetails[buyer].amount *
        multiplier *
        daysPassed) / 10000;

        return reward;
    }

    function unlockTokenFor(address buyer) internal {
        if (buyersDetails[buyer].amount > 0) {
            uint256 tokensforWithdraw = buyersDetails[buyer].amount +
            buyersDetails[buyer].reward +
            getBuyerReward(buyer);
            buyersDetails[buyer].amount = 0;
            buyersDetails[buyer].reward = 0;
            IERC20(saleToken).safeTransfer(buyer, tokensforWithdraw);
        }
    }

    function unlockToken() external saleStoped {
        require(
            buyersDetails[msg.sender].amount > 0,
            "Presale: No tokens to claim"
        );
        unlockTokenFor(msg.sender);
    }

    // Method to unlock all the tokens
    function unlockAllTokens() external onlyOwner saleStoped {
        for (uint256 i = 0; i < buyers.length; i++) {
            unlockTokenFor(buyers[i]);
        }
    }

    function withdrawAllSaleTokens() external onlyOwner saleStoped {
        uint256 amt = IERC20(saleToken).balanceOf(address(this));
        IERC20(saleToken).transfer(msg.sender, amt);
        totalTokensforSale = 0;
    }

    function withdraw(address token, uint256 amt) public onlyOwner {
        IERC20(token).safeTransfer(msg.sender, amt);
    }

    function withdrawAll(address token) public onlyOwner {
        uint256 amt = IERC20(token).balanceOf(address(this));
        withdraw(token, amt);
    }

    function withdrawCurrency(uint256 amt) public onlyOwner {
        payable(msg.sender).transfer(amt);
    }
}
20
  • 1
    Hi Mila, thank you for your response. I have edited my Question with the full code.
    – Aleksandar
    Commented Mar 8 at 18:58
  • 1
    Mila, thank you for your time. The account has way enough ERC20 tokens and also ETH on the account to cover the fee.
    – Aleksandar
    Commented Mar 10 at 17:12
  • 1
    the problem still exist and I dev not know why ^^
    – Aleksandar
    Commented Mar 14 at 14:13
  • 1
  • 1
    Mila, if you want you can contact me via mail - [email protected] I have used your updated source but now there are other errors ;-) I think it makes sense to discuss if you are available for a freelance offer.
    – Aleksandar
    Commented Mar 16 at 1:16

1 Answer 1

0

While I have not reproduced it with your contract specifically, I believe editing these lines could be helpful, and let's see whether it fixes the problem:

const tokenContract = getContract({
      address: token.address,
      abi: erc20ABI,
      client: walletClient
    });

(Here:)

erc20-viem-low-level-call-failed

There're no walletClient or publicClient arguments that the viem.getContract(...) method accepts.


I'm not sure if that helps, but please check that out and let me know. :)

1
  • 1
    Thank you again, Mila for your help. Now I get this error: Cannot read properties of undefined (reading 'allowance')
    – Aleksandar
    Commented Mar 15 at 10:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.