2

I have a method in my contract like so:

  mapping(address => uint256) allowance;

  ...

  function setAllowance(address user, uint256 amount) external onlyOwner nonReentrant returns (uint256) {
    allowance[user] = amount;
    emit AllowanceLimitChange(user, amount, ChangeType.None, allowance[user]);
    return allowance[user];
  }

In my truffle test, I am expecting it to return me a uint256, but it actually returns me a transaction object! Below is my test code:

  it("should set allowance limits to non-zero", async () => {
    try {
      const value = web3.utils.toWei("0.2");
      const newAllowance = await contract.setAllowance(accounts[1], value);
      assert.equal(value, newAllowance); // assertionError
    } catch (err) {
      assert(false); // always come here because of assertionError due to type mismatch
    }
  });

Screenshot of Transaction Object received: enter image description here

1 Answer 1

4

TLDR;

Similar to this issue here , use .call after the function name. Then it works out!

      const newLimit = await contract.setAllowance.call(kid1, value); // the .call() is the fix

Edit: (adding more details in the line of @GoodVibration's comments)


setAllowance is a non-constant call, which means it modifies contract state in some way. constant was used in older solidity code, which in modern versions are indicated via view (only reads state) or pure (no read from, or write to contract state).

When we invoke such a non-constant method, we will only get the transaction response as the return value, irrespective of what return value we might have coded in (in this case, uint256). Thus, we have two options:

  1. use .call : this sort of "evaluates" the code inside the method, but not cause a transaction to happen, and thus no state change will be persisted. A test case to help explain things better:
  it("will set allowance limits to non-zero within the scope of the method", async () => {
    try {
      const value = web3.utils.toWei("0.2");
      // https://ethereum.stackexchange.com/a/90342/22522
      const newLimit = await contract.setAllowance.call(accounts[1], value);
      assert.equal(value, newLimit); // we get a return value
    } catch (err) {
      console.log("error:\n", err);
      assert(false);
    }
  });

  it("proves that above call does not persist state change", async () => {
    const allowance = await contract.getAllowance(kid1);
    assert.equal(allowance, 0); // allowance did not change at all!!
  });
  1. Invoke the method, and use Events for testing: when we write await contract.method(... args) , that method is actually invoked, transaction goes through mining, and events are fired. While this is great, it will always return a Tx object as response, and not the return type you coded. As such you need to assert on the events that gets fired. something like this:
  // smart contract
  event AllowanceChange(address user, uint256 oldLimit, string delta, uint256 newLimit);
  
  ...

  function setAllowance(address user, uint256 amount) external onlyOwner nonReentrant returns (uint256) {
    allowance[user] = amount;
    emit AllowanceChange(user, amount, "set", allowance[user]);
    return allowance[user];
  }

      
  // Truffle JS Test
  const value = web3.utils.toWei("0.2");
  const result = await contract.setAllowance(accounts[1], value); 
    truffleAssert.eventEmitted(
    result,
    "AllowanceLimitChange",
    (ev) => ev.newLimit.toString() === value,
    "Allowance Change event should be triggered"
  );
4
  • This answer is generally wrong. Using .call is mandatory when you use web3.js directly, but not when you use it via Truffle. The function in context - setAllowance is a non-constant function (changes the state of the blockchain), hence the value returned to the offchain caller is the transaction receipt and not the actual return-value (which is returned only to onchain callers). Nov 20, 2020 at 16:23
  • And while your suggestion to use .call will allow this offchain caller (the test script) to obtain the actual return-value, it will also prevent the actual transaction from being executed (i.e., the state of the contract will remain the same). The correct way here is to obtain the returned-value through the event logs within the returned receipt. Nov 20, 2020 at 16:26
  • @goodvibration yes, I have updated my answer along these lines. anything more I should be adding ?
    – Somjit
    Nov 20, 2020 at 17:18
  • Looks really good now IMO. Nov 20, 2020 at 18:43

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.