What is the pattern for handling throw on a Solidity contract in tests
Asked Answered
A

5

9

I have a function on a Solidity contract that does a throw. Eg.

   function do(x,y)  {
        if ( msg.sender != owner )
            throw;
        // ...
   }

In the Truffle environment I have a test js something like:

//.... part of a promise chain
       .then(
            function (_bool0) {
                assert.isTrue(_bool0,"whoops - should be true");
                return contract.do( "okdoke" , {from: accounts[1]} );
            }).then(
            function (tx_id) {
                //..
                done();
            }
    // ...

The return contract.do() causes the condition that results in the throw. Which produces the following in the Truffle test output for this test:

Error: VM Exception while executing transaction: invalid JUMP

What is the idiom for handling a throw from a contract function in a test like this? The throw is the correct behavior.

Arlin answered 13/4, 2016 at 10:26 Comment(2)
Caveat for anybody following the "check if all gas is used" approach: This will change in the future, as throw is being deprecated in favor of "revert()". Revert() will NOT use up all gas, only the gas that has been used up to the point where revert() has been called.Barbarese
This problem still causes me trouble, especially since I'm using revert(). I was able to field for errors on TestRPC but now that I'm on rinkeby test network reverted transactions return as if they were successful. any updates on your side?Godbeare
M
4

The zeppelin project as an awesome way to do just that:

it("should fail to withdraw", async () => {
    try {
      await receiver.withdrawToken(0x0);
      assert.fail('should have thrown before');
    } catch(error) {
      assertJump(error);
    }
  });

function assertJump(error) {
  assert.isAbove(error.message.search('invalid opcode'), -1, 'Invalid opcode error must be returned');
}

https://github.com/OpenZeppelin/zeppelin-solidity/blob/master/test/Ownable.js To see a full example

Merous answered 2/8, 2017 at 18:1 Comment(0)
T
3

The 'most correct' solution to this problem I've been able to come up with is checking all the gas sent has been spent, which is what occurs on a throw, but there is an additional wrinkle to make the solution work on both TestRPC (which I am guessing you are using, given the actual error being thrown) and Geth. When a throw occurs in Geth, a transaction is still created, spending all the gas, but no state changes occur. TestRPC actually throws the error, which is useful for debugging purposes.

   //Somewhere where global functions can be defined
   function checkAllGasSpent(gasAmount, gasPrice, account, prevBalance){
       var newBalance = web3.eth.getBalance(account);
       assert.equal(prevBalance.minus(newBalance).toNumber(), gasAmount*gasPrice, 'Incorrect amount of gas used');
   }

   function ifUsingTestRPC(){
       return;
   }

   //Some default values for gas
   var gasAmount = 3000000;
   var gasPrice = 20000000000;

   ....

   //Back in your actual test
   it('should fail ', function (done) {
       var prevBalance;

   ....

   .then(function (_bool0) {
        assert.isTrue(_bool0,"whoops - should be true");
        prevBalance = web3.eth.getBalance(accounts[1]);
        return contract.do( "okdoke" , {from: accounts[1], gasPrice:gasPrice, gas:gasAmount } );
        })
    .catch(ifUsingTestRPC)
    .then(function(){
         checkAllGasSpent(gasAmount, gasPrice, accounts[1], prevBalance);
    })
    .then(done)
    .catch(done);

I'd cheerfully implement a more straightforward solution if another appears, though.

NB If you spend all of gas the with a transaction that is accidentally valid, this won't spot that - it will assume that the gas was spent due a throw inside the VM.

Trotskyite answered 13/4, 2016 at 13:18 Comment(1)
thanks for taking the time to think about it but I was looking more for how to handle the throw. The VM Exception just seems to break everything but I was hoping for more of a try/catch technique to keep control. Rather than everything fall apart.Arlin
M
0

Just to let everyone know, I came across this problem as well and have been using the following:

function getTransactionError(func) {
  return Promise.resolve().then(func)
    .then(function(txid) {
      var tx = web3.eth.getTransaction(txid);
      var txr = web3.eth.getTransactionReceipt(txid);
      if (txr.gasUsed === tx.gas) throw new Error("all gas used");
    })
    .catch(function(err) {
      return err;
    });
}

On geth it uses the transaction ID to get available gas and used gas and returns an error in case all gas was used. On testrpc it simply catches the thrown exception and returns it. I'm using it inside a test as follows:

return getTransactionError(function() {
    return contract.doSomething();
}).then(function(err) {
    assert.isDefined(err, "transaction should have thrown");
});

Naturally one can also leave out the catch, in which case the promise will simply fail with an error if it was thrown.

Minstrelsy answered 2/11, 2016 at 17:29 Comment(0)
U
0

In my opinion the cleanest way is:

it("should revert", async function () {
    try {
        await deployedInstance.myOperation1();
        assert.fail("The transaction should have thrown an error");
    }
    catch (err) {
        assert.include(err.message, "revert", "The error message should contain 'revert'");
    }
});
Untune answered 14/8, 2018 at 21:6 Comment(0)
M
0

Since this question was first asked, there have been many improvements to Solidity, Truffle, and the Ethereum development ecosystem as a whole, making it a lot easier to assert reverts and other throws.

My truffle-assertions library allows you to make assertions for any kind of Solidity throw or function failure in a very straightforward way.

The library can be installed through npm and imported at the top of the test javascript file:

npm install truffle-assertions

const truffleAssert = require('truffle-assertions');

After which it can be used inside the tests:

await truffleAssert.fails(contract.failingFunction(), truffleAssert.ErrorType.INVALID_JUMP);
Meshach answered 14/10, 2018 at 22:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.