The Resurrection of LLL: Part II

In part 1 of this series I introduced the idea that we may need to change the mindset of smart contract development to emphasize its difference from, well, almost every other type of software development. I suggested that perhaps LLL would be an appropriate choice of language to prompt this change.

In this article I will begin to describe a small dispatcher written in LLL that allows an associated contract to replace itself if it becomes necessary. The dispatcher passes all function calls through to the contract with which it was initialized. It uses delegatecall to call functions on its contract, which means the contract executes in the context of the dispatcher. This way, all storage and memory content is preserved for the contract in the event it's replaced.

Compatibility

The dispatcher and its example contract integrate with a standard web3 system; they are written in such a way that they emit events and accept function calls in the same way that Solidity contracts do. They also support function modifiers. I will describe exactly how to accomplish all of this as I believe it has never been done in LLL before, so it'll be new to just about everyone.

Setup

In order to do any development in LLL, you need an LLL compiler. If on Ubuntu you can simply add Ethereum's PPA and install lllc:

sudo add-apt-repository ppa:ethereum/ethereum  
sudo apt update  
sudo apt install lllc  

If you get an error indicating that you're missing add-apt-repository, do the following and try again:

sudo apt install software-properties-common python-software-properties  

To install lllc on other platforms is a more complex matter which is outside the scope of this article. The LLL compiler is contained in the Ethereum C++ client, cpp-ethereum. This page has extensive instructions on its installation for various platforms.

Having the compiler installed enables you to try things as I go through the source code. I'll be going deep into details so all will hopefully be clear once I'm done.

The dispatcher

The ideas behind this dispatcher are heavily influenced by Nick Johnson's Solidity-based function dispatcher, upgradeable.sol. The code that makes up this LLL-based dispatcher consists of three files: dispatcher.lll, constants.lll and utilities.lll. In actual fact, most contracts written using the dispatcher will include both constants.lll and utilities.lll. Both of these can be expanded as needed while writing your own contracts.

constants.lll

This source file contains convenient definitions of various things, such as memory and storage locations, true and false, and shorthand references to function signatures.

LLL has a macro system that facilitates constants like these using the def keyword. Here's an example of defining a constant:

(def 'new-address 0x60)

This allows you to use new-address in your code instead of 0x60, clarifying your intent. You can either use the single tick as in my example, or you can enclose the string in quotes:

(def "new-address" 0x60)

I prefer the tick as it is reminiscent of Lisp's macro system, though LLL's def is nowhere near as powerful as Lisp's defmacro. One important thing to keep in mind is that LLL macros will always be inserted inline. Going back to our example above, using new-address in your source will result in 0x60 being compiled into your contract. Likewise, this slightly more complex macro example:

(def 'only-owner
  (when (!= (caller) @@contract-owner)
    (jump invalid-location)))

will result in

(when (!= (caller) @@contract-owner)
  (jump invalid-location))

being compiled into your contract when you invoke it with only-owner.

These are simple examples; you can build up macros of any complexity you choose. But always remember: every time you invoke a macro, its entire code sequence will be inserted into your contract. Every time. These are not calls to a function. With that in mind, I can tell you that macros never return a value. They can't! They're inlined into your contract. A return in your macro would destroy the flow of your code—unless of course you are fully aware and intend for a return to be executed at that point in the code. But then, that's not actually a return from the macro.

Because macros are really instructions to the compiler, there's no cost to including them in your contract code. (include "utilities.lll") adds no size to your contract whatsoever. You can load up macro files with dozens of macros and unless you actually invoke one in your contract, it's as if they weren't included at all.

utilities.lll

This source file contains macros that are more than just convenient definitions of hard-to-remember numbers. It contains modifiers and functions that can be invoked from contracts to ease development and clarify program flow. The current utilities.lll defines a few essential macros that will be used in both the dispatcher and its example contract.

Solidity has the concept of function modifiers. It allows a function to simply refer to a modifier in its function signature:

function test(uint x) onlyowner returns (uint y)  

Function modifiers themselves are short bits of code that usually ensure a particular state and allow or disallow execution of the function depending on that state. An example from the function signature above would be:

modifier onlyowner {  
    if (msg.sender == owner) _
}

The odd use of an underscore simply indicates that the body of the function should be executed if the modifier evaluates to true. Here's the same modifier written in LLL:

(def 'only-owner
  (when (!= (caller) @@contract-owner)
    (jump invalid-location)))

This uses a couple of the constants defined in constants.lll, in particular, contract-owner and invalid-location. The @@ notation is shorthand for sload, meaning "load from storage". I won't be going into details of LLL syntax in future as this isn't an LLL tutorial. But this being the first example of LLL code I thought it would be prudent to have it explained fully.

The (jump invalid-location) statement causes the EVM to throw an exception, stopping contract execution and returning any ether sent to the contract. This exactly what Solidity's throw instruction does. The LLL modifier's behaviour is the inverse of the Solidity modifier but has the same effect: the body of the function is executed only if the caller is the owner of the contract.

To use the modifier in a contract, we simply refer to it before the rest of the function is executed:

(when (= function-id initialize)
  (seq only-owner ...

The preceding code will be explained in a future article. For now, we simply observe that only-owner is placed after the first seq in the function. If it evaluates to false, the rest of the function is not executed.

Conclusion

In part 3 of this series I'll finish the examination of utilities.lll, and then dive into the dispatcher itself.