Do you know that happy feeling when a transaction fails and you are the one that gets to debug it? Of course you don't.
Debugging transactions in Ethereum is still a major pain point. While there's been a lot of progress (and I hope to cover much of it in this series), it remains an unpleasant activity. So the more tools you have at your disposal, the better.
We'll start by exploring the data we get from etherscan. All of the transactions I'll use as examples were executed in Rinkeby, but everything should be valid for Kovan or Mainnet. I think. I hope.
(I'll also use eth-cli
for performing some simple tasks. You can install it by doing npm install -g eth-cli
, but you don't need to.)
Function selectors
Let's start with a very simple contract:
contract Box {
uint256 public value;
function inc() external {
value++;
}
}
Now let's say we are interacting with this contract somehow (maybe we are building a dapp for it) and our transaction fails. Check for example this transaction:
There isn't a lot of data here, but we can manage to get some useful information out of it.
If you click on "Click to see More", you'll see more data about this transaction. The part we care about now is the Input Data, a bunch of bytes that make up our transaction.
Input data always starts with a function selector: 8 characters (4 bytes) that identify the method being called. So you can start by checking that you are calling the proper method. In this case, since there's only one function, you can use eth method:hash 'inc()'
to find out its selector. Turns out it's 371303c0
, but in the failing transaction the 4 bytes are 33da8f9c
. So we found the problem!
In this case I artificially made the situation harder by using a weird name (it's incrementPlease()
, in case you care). But in other cases it's even easier.
In this transaction, etherscan correctly decodes the method being called.
I don't know exactly when it does this and when it doesn't (maybe it leverages the 4byte directory?), but in any case if the method being called is common enough, then it will probably be automatically decoded here, as in this case where we are calling increment()
instead of the correct one, inc()
.
Revert reasons
Let's add another method to our contract:
contract Box {
// ...
function dec() external {
require(value > 0, "Value must always be positive");
value--;
}
}
Now we have a require
with a revert reason. In this transaction we get an error, but we can easily guess what happened thanks to the message in the "Status" row.
This helps a lot, so the take-away is simple: always add reason strings to your revert
s!
Of course, if you don't include a reason string, like here, then you'll need to try other approaches.
Digression: how revert reasons work
Curious about how to recover the error manually? (Hopefully you will never have to do this.) First, we make an eth_call
with the same data, and at the same block number (0x5c471e
is 6047518
, the block number where the failed transaction was mined)
$ curl -H "Content-Type: application/json" -X POST --data \
'{"id":1, \
"jsonrpc":"2.0", \
"method":"eth_call", \
"params":[ \
{"from": "0x9A2015Ed446E7A7450b9175413DEb04Fe4e555c2", "to": "0x23c1fd51DD362D87A9E20F7370B7E9A0CbC40D4f", "value": "0x0", "data": "0xb3bcfa82"}, \
"0x5c471e" \
]}' https://rinkeby.infura.io/
{
"jsonrpc": "2.0",
"id": 1,
"result": "0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001d56616c7565206d75737420616c7761797320626520706f736974697665000000"
}
And then we interpret the result as the data of a call to the function Error(string)
:
$ eth method:decode 'Error(string)' '0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001d56616c7565206d75737420616c7761797320626520706f736974697665000000'
[
"Value must always be positive"
]
Invalid opcodes
Now let's add a third method to our contract:
contract Box {
// ...
function divideBy(uint256 x) external {
value = value / x;
}
}
Again, this is a very simple code, so you can probably guess what's coming, but let's do it anyway.
This transaction fails and we don't get an error message. Instead, there's an invalid opcode 0xfe
.
If you check the list of op codes you'll see that 0xFE
means "INVALID". If we decode the data that was sent:
$ eth method:decode 'divideBy(uint256)' 0x1fcc4afb0000000000000000000000000000000000000000000000000000000000000000
["0"]
Then the error is clear: there was a division by zero that triggered this. Another example of an invalid opcode being thrown happens when you try to access an array using and index that is out of bounds.
We had to manually decode the data because the contract is not verified. If it were, then etherscan would've decoded it for us.
Of course, in most real life scenarios your code won't be so easy to read as it was in these examples. In the next post, we'll continue to explore what can be done in those cases.
Top comments (0)