Development of smart contracts running on virtual machines for blockchain networks has expanded the possible applications of blockchain technology beyond that originally envisioned when Bitcoin was first proposed. Ethereum smart contracts, specifically because of the volume of transactions occurring on Ethereum, have emphasised these improvements in both theory and practical design of smart contracts as a whole. Now these programs can be utilised to support a wide-range of sophisticated financial operations known more generally as the field of DeFi. Such operations include but are not limited to: non-fungible tokens (NFTs), automated market makers (AMMs), stablecoins, flash loans, layer-2 applications (L2s), bridges, and swaps. One of the primary limitations to the design of these technological solutions for classical financial problems however is the tendency of smart contracts to be vulnerable to bugs and unintended flaws. Therefore, to balance the opportunity provided by the development of smart contracts with the increased possibility of exploitations is perhaps the key challenge of the future of DeFi. To adapt to this, retrospection must be made of some of the design choices made for smart contracts, and proposals for future design choices can thereafter be applied.
What is the object-oriented paradigm?
Using the object-oriented paradigm is geared towards perceiving the goals needed to solve a problem as sharing close to a one-to-one relationship with objects. This is to say that each object is strictly defined and denoted to ideally meet the requirements of a single goal each. Therefore there is an emphasis on creating templates that can ensure that any objects derived from said template will perform identically to each other in an identical environment. This ensures that where there exists an object to satisfy a goal, that goal is satisfied in an identical manner given no change in the environment. However, the behaviours assigned to each object are less strictly defined and therefore there is no guarantee that an object will perform as expected in a given environment without prior testing.
What is the functional paradigm?
Using the object-oriented paradigm is geared towards perceiving the goals needed to solve a problem as sharing close to a one-to-one relationship with behaviours. This is to say that each behaviour is strictly defined and denoted to ideally meet the requirements of a single goal each. Therefore there is an emphasis on ensuring that behaviours will always perform as expected regardless of the environment. Where there exists a behaviour to satisfy a goal, the goal can be expected to be satisfied regardless of the environment. However, the objects that behaviours are assigned to are less strictly defined and hence objects are not guaranteed to perform identically given an identical environment.
Is Solidity an object-oriented language?
Solidity is heavily modelled off the class/object structure defined for C++/Java. Smart contracts contain templates known as contracts (or known as classes in Java) that allow for objects to be instantiated from. Within each template there are fields and methods: fields are values that are statically-typed, and methods are functions that have signatures that indicate access rights, inputs, and outputs. Therefore objects can instantly and easily be replicated based off a single template and interacted with without interfering with other objects.
Is Solidity a functional language?
While most programming languages such as Solidity do support a number of features that do allow for a functional paradigm (after all neither object-oriented nor functional paradigms are mutually exclusive), Solidity has rather limited support. Especially of note is the lack of support for tail-call optimisation which is a feature that allows for recursive functions to replace loops in such a manner that does not congest the data stack. Without support for features such as these, Solidity cannot fully realise the potential of a functional paradigm.
What is a monad?
Monads are sections of a program that are able to abstract away elements that are not contained within the Turing-complete capabilities of functional paradigms. This includes elements such as I/O, permanent storage, and networks. By confining as much of these “impure” features to small sections of a program as possible, it is significantly easier to control bugs and possible attack vectors. Other actors using the program will also be able to assure themselves that the “pure” sections that do not rely on monads will work in any environment (given appropriate testing).
What is the ‘Expression Problem’?
To define this problem involves contrasting the intentions of the object-oriented paradigm and the functional paradigm. With no conclusive answer provided any developer would be asked by the ‘expression problem’ how they view the current and future states of a program. Given an unstable environment should a program always behave as intended at the cost of being strictly defined for a specific environment? As an environment evolves should a program seek to adapt by defining new objects to meet new goals in a solution of a problem, or should it seek to adapt by defining new behaviours instead? The binary state of these questions mean they can never be applied universally due to ignoring the flexibility of both paradigms, and their open-ended state allows for many interpretations. Hence, the ‘expression problem’ can be defined as a thought exercise.
Current State of Smart Contracts
Positive Elements of Smart Contracts
There are two important pillars that underlie the foundations of smart contracts as they have become, and that lend to the opportunities that can be extracted from them. Taking the long history of open-source program development and synergising with the ability of blockchains to provide decentralised execution and storage for these programs has created a unique concept. As such the two pillars that inform the viability of smart contracts can be denoted as “transparency” and “decentralisation”. Transparent code ensures that the list of behaviours defined for any given object are clear and concise; ideally ensuring that unintended side-effects caused by obscure programs do not occur and that an expected result always occurs. Decentralised autonomous organisations (DAOs) would not be able to hide funds or fail to pay for services in this scenario. In contrast, decentralisation as with the pattern of open-source development allows for a faster “evolution” of the code base due to opening of development to a range of individuals and the ability to rigorously test code. It is possible that the proliferation of NFTs would not have come about without the collaboration of many individuals to ensure a working standard to be used by the wider DeFi community. However, neither pillar can be relied upon to stand without support. If not properly accommodated with well-designed software patterns and appropriate auditing both these important positives can become negatives that hurt the future of DeFi. In other words smart contracts do not exist as a tool on their own that can be employed without caution, but as a tool that must further synergise with other tools just as open-source programs originally synergised with blockchains to allow for their existence in the first place.
Negative Elements of Smart Contracts
In Ethereum, the programming language Solidity exists as the primary form of creating and interacting with smart contracts. Solidity can be described as an object-oriented language, which is a particular paradigm dedicated to ensuring the ease of replicating set behaviours for numerous objects (more in-depth descriptions are provided later in this article). This is under the common view that in programming there are objects (i.e. wallets for holding currency) and behaviours (i.e. users can withdraw currency from a wallet). As discussed previously in regards to smart contracts it is a strength to ensure that the behaviours of any object are transparent and clear such that unintended side effects are unlikely to occur.
Illustrated depiction of a typical “flow” of behaviour that can be defined as “transparent” with no possibility of unintended alternative “flows”; also an example of a typical functional paradigm.
This is one of many integral elements to the pillar of “transparency”. However, while Solidity as a tool can be used to support this pillar, the object-oriented design it is derived from is not intended to be applied in this manner. Using this paradigm tends to remove significant control over the behaviours of objects in a smart contract environment, in favour of having greater control over the defining of the objects themselves. To this degree it is more difficult to guarantee that a behaviour will occur as expected. Hence the challenge of properly supporting “transparency” is made more difficult than necessary. The pillar of “decentralisation” tends to fall outside the scope of the control of smart contract developers, and is not directly impacted by which paradigm is exerted by said smart contracts. However, the synergy between “decentralisation” and “transparency” does come into importance when considering the heightened “evolution” of and interaction with smart contracts, and how therefore the “flows” of behaviour in a smart contract can rapidly adapt over time. All of the above summarisation of the current space in smart contract development however is not put forth to suggest that object-oriented designs are unsuitable for any or all situations, but that in some situations an alternative tool may be better suited.
There are many variations of implementations of object-oriented paradigms for programming, but Solidity’s particular implementation can best be compared to that of C++ and Java. All objects are defined as classes (each class is a template for objects) and can be called to be instantiated as a new object. Within each class are fields and methods. Fields are statically-typed to reduce the chance of unintended values being assigned, and methods are indicated by their signatures which denote what they return, who can access them, and what they require as inputs. This rigorous form of pre-mediated templates ensures that all objects instantiated from said template perform in an identical manner unless otherwise stipulated. This guarantees the “transparency” of objects as they can never be vaguely defined or access behaviours not previously defined. As emphasised earlier though it does not guarantee the “transparency” of behaviours. All objects of the same class can be expected to behave identically given an identical environment, but without testing every possible environment it is unclear if all objects will behave as expected in a given environment.
Depictions of how classes, fields, and methods are typically defined in an object-oriented environment.
In contrast, an alternative programming paradigm can be applied to resolve these situations. The functional paradigm is focused less on managing objects and instead on managing behaviours; pairing particularly well with the pillar of “transparency”. While all objects of the same class cannot be expected to necessarily behave identically given an identical environment, all objects will behave as expected in a given environment. This contrast between the two paradigms can be defined as the “expression problem”. Given this property the need for exhaustive testing of objects across all environments is more or less obsolete. More detail will be provided to substantiate exactly how programming languages using a functional paradigm can be used as a tool for supporting the pillar of “transparency” potentially more efficiently than object-oriented paradigms in a later section.
Initially though, to understand why object-oriented programming currently serves as the default approach to smart contract development, there is a requirement for a wider perspective of the field of DeFi. Programmers that begun development in this field had primarily begun learning how to program in traditional software or web environments. In these environments it has been the case for a couple of decades to approach problems that needed solving (where any programming can be visualised as the need to solve a problem) through strictly denoting what objects exist and then defining what behaviours must be assigned to each object. Where goals can be said to describe the necessary resolutions to solve a problem, and where there exists $N$ goals, it has been common to attempt to find $O$ objects where $O$ is as close to $N$ as possible. Concrete definitions of goals as a series of objects is most closely-aligned to the aforementioned style of problem-solving. Such approaches were especially permitted in instances where it wasn’t typical to promote “transparency” or “decentralisation” and to instead tightly control the environments in which programs would operate. Given what has been concluded previously in regards to the “expression problem” it can be observed that if it was possible to restrict all environments a program would execute within to one, all objects of the same class will execute identically and as expected. Hence, combining the most optimal of both object-oriented and functional paradigms.
Additionally, less strict parameters for denoting behaviours for an object helpfully expands the computation that can possibly be performed. For instance the management of I/O, permanent storage, and networks as examples are easier to implement when behaviours are less strictly defined, however this does come at the potential cost of sacrificing important elements of Turing-completeness. With no appropriate parameters it is possible that a program will never finish execution, or that it might execute in an unintended manner. While most programs cannot entirely avoid elements such as I/O and permanent storage, they can limit it to an extent that a majority of the program may be considered “more” Turing-complete. In the functional paradigm this involves the use of monads to abstract away these mechanics into more manageable portions. However, implementing monads is more difficult to perform. Therefore, despite the instability of the environments a smart contract may execute within, these reasons accumulated to contribute to the emphasis on object-oriented design for smart contract development.
As defined earlier the expression problem contends with the distinction between defining and developing objects or behaviours in a program. Will a smart contract opt to guarantee strictly defined objects that behave identically given an identical environment, or opt to guarantee strictly defined behaviours that will work as expected in all environments? Throughout the development and subsequent evolution of a smart contract is it intended that the number of objects defined and the types of objects defined will increase, or that the behaviours performed by given objects will adjust? These questions are deliberately binary and as such are ignorant of the potential capability for any program written in any paradigm to conform to any vision. Equally, the prior questions cannot be thought to encourage any particular conclusion as to the answer for any debate of paradigm choice.
An abstract way of visualising differences in confrontation of the ‘expression problem’ between object-oriented and functional paradigms; note the contrast between objects on the left with unique behaviours, and objects on the right with many behaviours but no clear uniqueness.
It cannot be said that either of the alternatives are mutually exclusive, but that each paradigm is more cultivated for a particular approach. Similar to the case of object-oriented design, the functional paradigm assumes that $N$ goals must be satisfied to ensure the resolution of a problem, but take the approach instead of looking to strictly define $B$ behaviours where $B$ is as close to $N$ as possible. Therefore while the possibilities of what can be achieved through a behaviour is somewhat limited, the transparency of each behaviour means that it is simple to test what will occur in any given environment. It should additionally be noted that both paradigms when taken to their intended conclusions (where $O$ or $B$ are as close to $N$ as possible) are inherently modular designs which are most optimal when aligned with the pillars of “transparency” and “decentralisation”. Monolithic designs (which are not as optimally suited for these pillars) require their own emphasised paradigms.
Properties of the Functional Paradigm
One of the core properties of the functional paradigm is that values are immutable and cannot be changed once they are declared; only copied or changed by assigning a new value. This optimally aligns with the pillar of “transparency” as it is an important philosophical foundation of blockchain that data cannot be removed or altered at a later date. Any capability to mutate existing values allows for the possibility of hiding or altering data in a potentially malicious manner. Attempts to take input or produce output, or perform other hidden effects, cannot be performed opaquely without some form of detection. The primary exception to this is once again the use of monads which allow for sections of a program to “break” some of the rules of the functional paradigm - but monads also draw attention to which sections of a smart contract must be specifically audited and treated with caution.
Examples of immutability (no permanent state) and referential transparency as applied in a functional paradigm in contrast to an object-oriented paradigm.
Degree of Transparency
Similarly, referential transparency is another core property of the functional paradigm that lends support to the pillar of “transparency” in smart contracts. Due to the decentralised nature of blockchain networks no guarantees can be made of the environment a smart contract executes within. This includes no guarantees of the ordering of behaviours, which actors (where an actor is an individual interacting with the smart contract) call which behaviours, and if any given behaviour will finish executing before or after another behaviour. These uncertainties have helped make many attacks possible, including the infamous “DAO Attack” which resulted in the need for an Ethereum hard fork. In this instance it was assumed that any actors would call one behaviour, and that the smart contract would be able to adopt control over the flow of behaviours from there. However, not only could an actor call any behaviour at any moment they desired, perhaps circumventing the intended order of behaviours, there was also no guarantee that each time a behaviour relied upon some validation check or attribute of the Ethereum Virtual Machine (EVM) that the expected result would occur. Hence the attacker in this case was able to discover a behaviour that could be called which would never complete validation checks before performing a transfer of funds on the EVM. This case is not an isolated instance of how these attributes of object-oriented smart contracts can be manipulated.
Demonstration of how unclear “flows” of behaviour can lead to unintended consequences, such as recursive calls of a single method prior to any validation.
Therefore it is important for any given behaviour to operate as expected in any given environment, and to perform identically given an identical environment. This security of a behaviour can be conceived as a scale of “functional purity”, wherein a function of sufficient purity can be called in any order without causing any unintended events or allowing for attacks to occur. It is not only “transparency” that can be increasingly guaranteed by “functional purity”, but also cheaper gas costs potentially as a consequence of memoisation. If it is known clearly in advance that a given behaviour will output a particular result given a particular environment, then a list of common results can be saved for later access. For sufficiently complex behaviours it might cost significantly more gas to execute than to store and access a value from memory (which is also guaranteed to be immutable).
Neither object-oriented nor functional paradigms can be considered the absolute answers to developing smart contracts for blockchain networks based on the pillars of “transparency” and “decentralisation”; however, in certain cases functional paradigms as an alternative to the current default could make it easier. The philosophical foundations of blockchain happens to align more or less with the philosophical foundations of the functional paradigm. In situations where I/O or permanent storage (or other similar mechanics) have either a reduced need or aren’t needed in a smart contract at all, the possibility of bugs or exploits to be attacked in a program can be reduced by adhering to the functional paradigm principles. Immutability and referential transparency in particular are important in providing guarantees that a given smart contract will operate without fault in any feasibly possible environment (the existence of monads not included). For all sections of a program not indicated to be a monad any actor could be assured of the safety of said sections of the program. In addition the emphasis on reducing permanent storage and allowing for memoisation may possibly allow sophisticated behaviours to be performed in an affordable manner considering the gas constraints of networks such as Ethereum.
Before these concepts can be readily applied to wider smart contract development however it must be declared that as the EVM exists currently there is no support for tail-call optimisation. Tail-call optimisation allows for the creation of recursive functions to replace loops in a manner that does congest the data stack (which would typically cause memory costs to become infeasible). Without this mechanic the amount of computation that can be performed in a functional paradigm is significantly reduced. Therefore to fully realise the potential of supporting smart contract development through the use of the functional paradigm will require changes in the EVM through EIPs.