1

I am using examples in soliditylang.org special functions

My simple contracts are extracted from the link

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.2 <0.9.0;

contract TestPayable {
    uint x;
    uint y;

    fallback() external payable {
        x = 1;
        y = msg.value;
    }

    receive() external payable {
        x = 2;
        y = msg.value;
    }
}

contract Caller {
    function callTestPayable3(TestPayable test) public returns (bool) {
        (bool success, ) = address(test).call{value: 2 ether}("");
        require(success, "low-level call failed");
        return true;
    }
}

My deploy script is using hardhat-deploy

const { network } = require("hardhat");

module.exports = async ({ getNamedAccounts, deployments }) => {
  const { deploy, log } = deployments;
  const { deployer } = await getNamedAccounts();
  console.log("deployer : ", deployer);

  for (const contractName of ["TestPayable", "Caller"]) {
    const arguments = [];

    const contract = await deploy(contractName, {
      from: deployer,
      args: arguments,
      log: true,
      waitConfirmations: 1,
    });

    const chainId = network.config.chainId;
    console.log(
      `deployed ${contractName} at ${contract.address}, chainId: ${chainId}`
    );
  }
};

module.exports.tags = ["all"];

My test script is

const { network, deployments, ethers } = require("hardhat");

!["localhost", "hardhat"].includes(network.name)
  ? describe.skip
  : describe("Unit Tests", function () {
      let testPayable, caller, deployer;

      beforeEach(async () => {
        accounts = await ethers.getSigners();
        deployer = accounts[0];
        await deployments.fixture(["all"]);

        testPayable = await ethers.getContract("TestPayable", deployer);
        caller = await ethers.getContract("Caller", deployer);
      });

      it("caller.callTestPayable3", async function () {
        await caller.callTestPayable3(testPayable.address);
      });
    });

My hardhat.config.js is

require("@nomicfoundation/hardhat-toolbox");
require("hardhat-deploy");

module.exports = {
  solidity: "0.8.18",
  namedAccounts: {
    deployer: {
      default: 0,
    },
  },
};

My package.json is

{
  "dependencies": {
    "hardhat": "^2.14.0",
    "hardhat-deploy": "^0.11.28"
  },
  "devDependencies": {
    "@ethersproject/abi": "^5.7.0",
    "@ethersproject/providers": "^5.7.2",
    "@nomicfoundation/hardhat-chai-matchers": "^1.0.6",
    "@nomicfoundation/hardhat-network-helpers": "^1.0.8",
    "@nomicfoundation/hardhat-toolbox": "^2.0.2",
    "@nomiclabs/hardhat-ethers": "npm:hardhat-deploy-ethers",
    "@nomiclabs/hardhat-etherscan": "^3.1.7",
    "@typechain/ethers-v5": "^10.2.0",
    "@typechain/hardhat": "^6.1.5",
    "chai": "^4.3.7",
    "dotenv": "^16.0.3",
    "ethers": "5.7.2",
    "hardhat-gas-reporter": "^1.0.9",
    "solidity-coverage": "^0.8.2",
    "typechain": "^8.1.1"
  }
}

Deployment was fine, but somehow, when I ran "yarn hardhat test", I got error

Error: VM Exception while processing transaction: reverted with reason string 'low-level call failed'
    at Caller.callTestPayable3 (contracts/fallback_call.sol:22)
    at processTicksAndRejections (node:internal/process/task_queues:95:5)
    at runNextTicks (node:internal/process/task_queues:64:3)
    at listOnTimeout (node:internal/timers:533:9)
    at processTimers (node:internal/timers:507:7)
    at HardhatNode._mineBlockWithPendingTxs (C:\Users\...\node_modules\hardhat\src\internal\hardhat-network\provider\node.ts:1840:23)
    at HardhatNode.mineBlock (C:\Users\...\node_modules\hardhat\src\internal\hardhat-network\provider\node.ts:517:16)
    at EthModule._sendTransactionAndReturnHash (C:\...\node_modules\hardhat\src\internal\hardhat-network\provider\modules\eth.ts:1532:18)

This is the most basic example of low-level call from Solidity site, supposedly to work out of box. Why doesn't it work for me?

2 Answers 2

2

TestPayable dont have enough Ether to cover the 2 Ether transfer that was indicated in callTestPayable3 .

We could send Ether to the TestPayable contract within the test like this:

const { network, deployments, ethers } = require("hardhat");

!["localhost", "hardhat"].includes(network.name)
  ? describe.skip
  : describe("Unit Tests", function () {
      let testPayable, caller, deployer;

      beforeEach(async () => {
        accounts = await ethers.getSigners();
        deployer = accounts[0];
        await deployments.fixture(["all"]);

        testPayable = await ethers.getContract("TestPayable", deployer);
        caller = await ethers.getContract("Caller", deployer);
      });

      it("caller.callTestPayable3", async function () {
        await deployer.sendTransaction({
          to: testPayable.address,
          value: ethers.utils.parseEther("5"),
        });

        await caller.callTestPayable3(testPayable.address);
      });
    });
2
  • thank you. I tried your solution. I added code to confirm the 5 eth went through. I still got same error. However, I do like your direction; your suggestion does make sense. But should I instead give 'caller' contract 5 eth? because it is caller to send the eth to testPayable. Anyway, I tried to replace testPayable with caller in the added code, then I got "Error: cannot estimate gas; transaction may fail or may require manual gas limit"
    – oldpride
    May 6 at 13:11
  • now your solution works after I added "receive() external payable {}" into the caller contract - this made caller to be able to receive the 5 eth. I will summarize my solution.
    – oldpride
    May 6 at 13:36
0

I didn't expect to go through so much hassle to run a example from the official solidity site.

Luckily @Saxtheowl provided the direction. It had a couple typo. Here I summarize the final code

In the test js file, give the caller contract 5 eth.

const { network, deployments, ethers } = require("hardhat");

!["localhost", "hardhat"].includes(network.name)
  ? describe.skip
  : describe("Unit Tests", function () {
      let testPayable, caller, deployer;

      beforeEach(async () => {
        accounts = await ethers.getSigners();
        deployer = accounts[0];
        await deployments.fixture(["all"]);

        testPayable = await ethers.getContract("TestPayable", deployer);
        caller = await ethers.getContract("Caller", deployer);
      });

      it("caller.callTestPayable3", async function () {
        await deployer.sendTransaction({
          to: caller.address,
          value: ethers.utils.parseEther("5"),
        });
        const Balance2 = await caller.provider.getBalance(caller.address);
        console.log("Balance2 : ", Balance2.toString());

        await caller.callTestPayable3(testPayable.address);
      });
    });

In the .sol file, I added "receive() external payable {}" so that caller can receive the 5 eth.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.2 <0.9.0;

contract TestPayable {
    uint x;
    uint y;

    fallback() external payable {
        x = 1;
        y = msg.value;
    }

    receive() external payable {
        x = 2;
        y = msg.value;
    }
}

contract Caller {
    function callTestPayable3(TestPayable test) public returns (bool) {
        (bool success, ) = address(test).call{value: 2 ether}("");
        require(success, "low-level call failed");
        return true;
    }

    receive() external payable {}
}

Your Answer

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

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