Intro (history – skip if in a hurry)
I’ve recently been busy writing a
pygments lexer for Solidity.
This has been an unusual learning process. Instead of getting
familiar with known tropes for a language, I got a view of what’s
possible and how it’s done on a lower level.
For example, here’s a little (as of yet untested) trick to keep multiple references to the same object in a mapping. (Relevant: post on references and mappings by Peter Vessenes, published on the same day that example was written.)
Anyway, it also occured to me yesterday that another under-used feature of the language could allow conditionally “reversing” undesired state changes that occured during dapp execution.
This has implications. Consider a dapp that modifies the state (changes
its own globals, makes
call()s), inspects the state
after execution, and resets to a previous state if the results are
unacceptable, - then continues with the rest of its code.
This is not specific to Solidity. In fact, reverting state changes upon
unsuccessful termination for the failed call only is the default mode
of operation for the EVM. Solidity doesn’t have
throw, so the compiler wraps every external call
in a structure that makes the parent fail if the child failed, – to
stay on the safe side. I applaud those who had the foresight.
Also, did you know? The guaranteed failure of a
throw is achieved
JUMPing to an invalid destination.
external is a keyword (primer – skip if acquainted)
The under-used feature mentioned before is a function visibility
external. It marks a function as “to be used by other
dapps, not this one”. From the docs:
The expression this.g(8); is also a valid function call, but this time, the function will be called “externally”, via a message call and not directly via jumps. Functions of other contracts have to be called externally. For an external call, all function arguments have to be copied to memory.
(The docs currently lack an example use case.)
Why isn’t it used much?
Probably because functions are
public by default; meaning their
signatures are available for other dapps to use, and external calls
is the only way dapps do it. Making function F
external instead of
public in dapp D prevents the developer of D from making direct calls
to it (via
JUMPs on assembly level), but the outside developer can
D.F() just the same.
external limits the developer of D, but
not everybody else, so its usefulness is not self-evident.
An “external call” manifests on the blockchain as an “internal transaction” (counter-intuitive phrasing, yes) from account A to account B, with gas (and possibly value) attached. EVM-wise, arguments are pushed to memory, “depth counter” is incremented, and control is transferred. If execution succeeds, return values are pushed to memory, and control is returned (together with all remaining gas). If execution fails, all performed state changes are rolled back, and control is returned (remaining gas, if any, is consumed).
Application (the trick – read and understand)
By making an external call to itself, a dapp could have an isolated part that either executes as expected, or not at all. “As expected”, of course, is loaded, since the expectations need to be specified somewhere.
(UPDATE 2016-08-09: it’s been noted that
external is not needed to make an “external call”. It is useful
in this case, however, to prevent making an internal call by accident.)
(UPDATE 2016-10-16: using assembly is not required, a Solidity
.call() can be performed instead. I find it useful here to demonstrate
what’s going on under the hood.)
doCall() is just a wrapper to work around Solidity’s security
failSend() demonstrates utility of the approach.
- can only be called externally due to the
- can’t be called by other dapps because of the
In effect, it can only be called by the same contract, via an external call.
The function body is all placeholders to increase readability. In a “real” compratmentalised call, one would first perform checks, preferably via modifiers; then changes to contract storage; then nested external calls. That is, the check-change-send routine.
After that, one would perform “state checks” and look for unexpected state changes, for all accounts touched by this function. If there are any discrepancies, changes resulting from all nested calls can be reverted – at the cost of gas initially passed to the call. The rest of the contract can continue execution.
This is far from fool-proof. Re-entrancy checks would still be required, along with everything else we have and haven’t yet come up with. But it demonstrates a feature of Solidity not yet available, namely exception handling – admittedly, in a convoluted manner.
(UPDATE 2016-08-11: an exception-like technique
has been brought to my attention that provides functionatilty similar to
It’s alive! (testing – read and verify)
I’ve deployed the dapp on ETH main-net (see etherscan.io or live.ether.camp). There were a few failed iterations, and then a final working one on Morden test-net, but I couldn’t get the block explorers to validate code there, so had to re-deploy the final on main-net again.
Storage variables after creation:
doCall() once, then check variables:
failSend() threw, so
numcallsinternal++ got reverted.
42 wei that were initially passed to
doCall() remained with the
Donate (please do)
Research, testing and writing the article took ~ 12 hours.