The entire operation did not revert because enough gas was provided by the transaction origin of the overall transaction. If insufficient gas was provided, then you would observe the behavior you were expecting, where both internal transactions would be reverted.
The second internal transaction was reverted because it ran out of gas. To detect this, the function that was called could include a true
return value (use a tuple if needed). The caller can explicitly check for this true
value, because an out of gas would return the default value which is 0 (false
).
Here's an example of what happened. First (internal) transaction requires 200 gas and second transaction requires 700 gas. Overall transaction was provided with 1000 gas. The gas that was made available to the second transaction, was less than 700. All gas was not fully provided to the second internal transaction. See bottom NOTE for explanation.
Here are some things that may help:
- changing the order of the operations, as you suggest, may help because the transaction that ran out of gas, will now be first and have all available gas to start with (instead of requiring refunded gas)
- keeping the operations in the same order, but for the first transaction limit the gas sent with the function call via
.gas(amount)
syntax. This could then allow the second transaction more gas. (In the example above, it could be explicitly limiting the first internal transaction to 220 gas, so that refunded gas is only 20.)
The simplest solution is for the transaction origin to provide more gas in the transaction. There is little downside since all unused gas will be refunded, and with plenty of gas potentially available for internal transactions, edge cases may be avoided.
NOTE why "all" gas is not provided to a call (internal transaction):
chriseth (Solidity author) said:
This is one of the quirky places of the EVM. You cannot just say "send
all my gas along with the call", you always have to give an explicit
number. We do have the GAS opcode which gives us the amount of gas
still available, but the problem is that performing the call itself
also costs some gas, so we have to subtract the amount of gas the call
costs from the value provided by GAS, and we also have to subtract the
amount of gas it costs to perform the subtraction...
EDIT: To clarify possible misinterpretations 1) contracts never pay gas -- they can reserve/delegate/limit the gas for another contract call. 2) Reverted transactions are still included in blocks and pay gas.