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 send()s and 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 try-throw-catch yet, just 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 by JUMPing to an invalid destination.

external is a keyword (primer – skip if acquainted)

The under-used feature mentioned before is a function visibility specifier, 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 still 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.)

Here’s a proof of concept (also on GitHub, or pygmentized). Details after the code.

(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.)

ThisExternalAssembly.sol:

contract ThisExternalAssembly {
    uint public numcalls;
    uint public numcallsinternal;
    uint public numfails;
    uint public numsuccesses;
    
    address owner;

    event logCall(uint indexed _numcalls, uint indexed _numcallsinternal);
    
    modifier onlyOwner { if (msg.sender != owner) throw; _ }
    modifier onlyThis { if (msg.sender != address(this)) throw; _ }

    // constructor
    function ThisExternalAssembly() {
        owner = msg.sender;
    }

    function failSend() external onlyThis returns (bool) {
        // storage change + nested external call
        numcallsinternal++;
        owner.send(42);

        // placeholder for state checks
        if (true) throw;

        // never happens in this case
        return true;
    }
    
    function doCall(uint _gas) onlyOwner {
        numcalls++;

        address addr = address(this);
        bytes4 sig = bytes4(sha3("failSend()"));

        bool ret;

        // work around `solc` safeguards for throws in external calls
        // https://ethereum.stackexchange.com/questions/6354/
        assembly {
            let x := mload(0x40) // read "empty memory" pointer
            mstore(x,sig)

            ret := call(
                _gas, // gas amount
                addr, // recipient account
                0,    // value (no need to pass)
                x,    // input start location
                0x4,  // input size - just the sig
                x,    // output start location
                0x1)  // output size (bool - 1 byte)

            //ret := mload(x) // no return value ever written :/
            mstore(0x40,add(x,0x4)) // just in case, roll the tape
        }

        if (ret) { numsuccesses++; }
        else { numfails++; }

        // mostly helps with function identification if disassembled
        logCall(numcalls, numcallsinternal);
    }

    // will clean-up :)
    function selfDestruct() onlyOwner { selfdestruct(owner); }
    
    function() { throw; }
}

Here, doCall() is just a wrapper to work around Solidity’s security mechanism, whereas failSend() demonstrates utility of the approach.

Note that failSend():

  • can only be called externally due to the external specifier;
  • can’t be called by other dapps because of the onlyThis modifier.

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 try-catch-finally.)

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.

Set up geth:

> var demoaddr = "0x6abd2b75ff5f306a4d99bfab1ff84b57bb9d23e7";
> var demoabi = <snipped>;
> var d = eth.contract(demoabi).at(demoaddr);

Storage variables after creation:

> console.log(d.numcalls(), d.numcallsinternal(), d.numfails(), d.numsuccesses())
> 0 0 0 0

Run doCall() once, then check variables:

> var lasttx = d.doCall(50000, {from: owneraddr, value: 42, gas: 200000})
>
> console.log(d.numcalls(), d.numcallsinternal(), d.numfails(), d.numsuccesses())
> 1 0 1 0

As expected, failSend() threw, so numcallsinternal++ got reverted. 42 wei that were initially passed to doCall() remained with the dapp.

Decided to try Steemit to see how much I’d get rewarded. Other ways are preferred, though.

Research, testing and writing the article took ~ 12 hours.