Radix Engine and Components: Reduce DeFi Hacks, Exploits & Failures
Originally posted 4th December 2020 Radixdlt.com/blog
DeFi applications are rapidly innovating new ways of providing more efficient, transparent, and democratic financial services. But they have also suffered an unfortunate sequence of high-profile losses of funds. Simple programming errors and programming language quirks that normally would only cause annoyance in a general application have instead created documented openings for exploits and system failures. Even where there are no bugs in smart contract logic, connections between multiple complex DeFi have made it nearly impossible to design smart contract code to cover all edge cases that could be manipulated by an adversary.
A more purpose-built alternative to typical smart contracts is needed before DeFi is ready for mainstream adoption. The Radix Engine and its “Component” model of development finally provides this solution for DeFi developers.
An Alternative to Ethereum-style Smart Contracts
“Smart contract” has become a generic term to refer to application code that a developer writes to deploy to a distributed ledger. Following Ethereum’s pioneering use of smart contracts to make blockchains more programmable, there has been an explosion of new smart contract blockchain platforms that largely replicate the specific implementation adopted by Ethereum: that is, a general purpose Turing-complete language and virtual machine running on-ledger. Tezos, EOS, NEO, Tron, Hashgraph, Hyperledger, and more generally follow this model.
Building a reliable financial application, however, is a different class of problem than building a game, web service, or other general application. A DeFi application deployed to a DLT network is expected to run autonomously, trustlessly, and irreversibly while managing millions of dollars in assets. Developers building to specialized requirements like these typically use specialized development environments to make it as easy as possible to avoid bad results.
The Radix Engine development environment is designed specifically for the creation of logic that defines predictable, correct results on-ledger in response to requests. This form of DLT programmability is based on Finite State Machines (FSMs), a class of solution that is common in mission-critical embedded systems where predictable correctness is the first priority. To make clear the difference from traditional Ethereum-style smart contracts, we give Radix Engine smart contracts a name more suggestive of their function: Components.
Let’s compare the typical smart contract approach to Radix Engine’s Components.
The Ethereum Smart Contract/Method Model
An Ethereum smart contract can be thought of as a black box deployed by a developer to the network. Inside that box you can imagine a little general purpose computer server running some code. To make that server-in-a-box do something, it offers “methods” that users or other apps can call by sending a signed message. The contract has its own internal variables inside the box that it can update based on the messages it receives via its methods.
Those internal variables are used by the developer to represent all sorts of things. For example, an ERC-20 smart contract creates something that behaves like a supply of tokens by maintaining an internal list of balances. “Sending” a token to somebody really means using a “send” method that the contract code translates into twiddling its internal Token balance variables — ie. reducing the sender’s balance and increasing the recipient’s.
This model is quite flexible, allowing anything, in theory, to run on a decentralized platform — thus the Ethereum vision of the “world computer”.
One problem however is that it puts a significant burden on the developer to ensure that their “representation” of tokens, or whatever else, within their smart contract is always correct, and that updates to the internal variables match intuitive expectations. This is doubly critical with DeFi, where potentially very expensive unexpected outcomes are immutably committed to a trustless ledger. And the situation becomes much more complex with DeFi applications where one transaction involves multiple composed smart contracts. In this case, one contract may call the methods on other contracts, with each updating their respective internal variables to produce a combined result.
For example, even a simple liquidity “pool” smart contract can quickly get complex, with results scattered across multiple contracts. If we want our pool to accept an existing (ERC-20) TokenA into the pool and mint a corresponding calculated amount of a TokenS representing a share of the pool, we end up with something like this:
These “tokens” don’t behave like the highly composable financial assets that we would expect. What we would prefer to think of as “tokens moving in and out of a pool” is instead expressed as negotiated balance updates across a network of black-box contracts.
It starts to look a bit like the traditional world, with siloed banks communicating with each other, but each keeping privately-held books.
With greater numbers of connections between black boxes, the network of contract calls explodes and correct behavior becomes more and more difficult to reason about and predict. A wide variety of exploits of Ethereum DeFi smart contracts come down to an adversary manipulating the fact that there is nothing fundamental stopping smart contract code from doing anything to its internal state, often with unintuitive results that may cascade through the system.
The Radix Engine Component/Action Model
The Radix form of smart contracts, Components, are built in a way that more closely models real-world expectations for finance (and other transactional systems that ledgers are good for). Components are built from finite state machine logic, and define their behavior by Actions that directly translate a discrete existing input (or “before”) state to an output (or “after”) state.
More concretely, this means that Components are defined by what it is possible for that Component to do via its Actions. By defining Components by their Actions in this way, we gain two important attributes when trying to avoid bad results.
First, Components can behave more intuitively like physical assets or other finance building-block “primitives”, rather than as black boxes, making their behavior via Actions easier to reason about, design, and analyze. Second, usage of Components is similarly more intuitive and predictable — and we gain the ability for creators of Component transactions (whether a front-end app, or by reference from another Component) to directly set their own definitions of what should be possible for that transaction, creating explicit guard rails on what can and cannot happen in creating the final output state.
Explaining how Components and Actions work is perhaps best done through a few examples:
A User-created Token
Take the example again of a token. On Radix, developers don’t need to use a monolithic ERC-20-style smart contract that keeps a list of all balances; we model each individual indivisible token (each “Satoshi” in Bitcoin terms) as a discrete independent Component. That token Component’s primary available Action is: “change my owner, if you have the right to do so”. So for example, the “before” input state may be owned by Alice, the “after” output state owned by Bob — if the conditions of ownership change defined in the token Component’s Action are met. This means that the token Component is defined by what it is possible for it to do: to be owned by different people.
Using the FSM-based Radix Component model, each token can be its own independent Component with Actions (like “change owner”) that define their behavior to match intuitive expectations.
While the Radix ledger may, behind the scenes, store a large number of these indivisible tokens collectively for efficiency, logically each token acts intuitively like a physical coin. That is, the defining capability of each is that its ownership can be passed from one person another. There is no question of it being accidentally cloned, or a glitch in smart contract logic causing tokens to no longer be accessible.
A supply of these tokens can be created by another component that manages the entire supply, while letting each individual token be sent around freely and intuitively.
A Liquidity Pool that Operates With Other Components Atomically
Not only may Components and their Actions be used individually, multiple of them may be combined within a single transaction. In this case, the mapping from input to output is collective, defined by all of the included Actions simultaneously. For example, let’s consider a “liquidity pool” Component. A simple version could be defined by these Actions:
- Deposit Action: “I am the owner of some reserves of TokenA and mint a proportional pool share TokenS to anyone who sends TokenA in.”
- Withdraw Action:“I am the owner of some reserves of TokenA and send a proportional amount of TokenA to anyone who sends me pool share TokenS (which I burn).”
While these are pseudo-code descriptions of the Actions’ definitions, you can see that the words in blue indicate Actions of other existing Components (TokenA, TokenS) that need to be included in a pool transaction.
A nice property of FSM-driven Actions is that the input → output mappings of these multiple Actions can be combined and operated simultaneously (rather than chained together sequentially as with smart contracts). When multiple Components are required for a single transaction, all of the Action-driven state changes of the various Components in the transaction mesh together like gears in a gearbox. As long as all of the gears of all of the relevant Components can all successfully turn together (the input and output mappings do not conflict), the transaction is successful. If they can’t, the entire transaction safely and correctly fails.
To compare with the Ethereum case above, a Radix Engine pool deposit transaction looks more like this:
A pool Component Action (deposit) includes the logic specifying that it expects to become the owner of some TokenA Components, mint a corresponding quantity of TokenS “share” Components, and make the user the owner of those.
This behaves more like an open platform of intuitively composable assets that we want!
From a front-end developer’s perspective, using the “deposit” Action of the Pool Component is no more complex than using a method on a traditional smart contract. But that Action itself can define the other Components that need to be involved (some specific TokensA and TokensS) — that is, which Components need to mesh their gears together for the transaction to be successful.
In this example, the “deposit” includes changing of ownership of a discrete set of relevant tokens using their Actions. Intuitively, the “mint” or “change owner” Actions may have their own intuitive rules about who can do those things, or who the recipient can be. If the Pool tries to do something that fails to meet those rules, the user’s request to the Pool correctly fails with clear rationale.
A Transaction Request with Consumer Safety Limits
To see how the consumer of Component Actions can explicitly prevent bad results, let’s take the example of a Token Swap Component. This Component accepts TokenA and returns an amount of TokenB equal to a market price (perhaps provided by an oracle or some market maker logic — the specifics don’t matter here). Its primary Action might look like:
- Swap Action: “Any receipt of TokenA will create a send of TokenB by the equation: TokenA * [currently defined B/A rate]”
A user wants to perform a swap using this Action, but would prefer to set his own clear limits on the acceptable swap exchange rate rather than rely completely on whatever the rate happens to be when the transaction goes through.
The Action model allows the user (ie. the consumer of the Token Swap “swap” Action) to also specify their own Action-style rules on the input → output mapping, as follows:
- User request: “I am sending TokenA to the “swap” Action of the Token Swap Component, which must result in a send to me of TokenB of at least [desired limit]”
The Action-like conditions of this user request create another “gear” (for that transaction only) that must mesh with those of the relevant Components and successfully turn along with them, or else the entire transaction will correctly fail. This means that the user can place absolutely clear and direct guard rails on their usage of any Component, without having to understand the internal details of the Component’s functionality. And these safety guard rails are available not just to front-end users but to the referred usage of Components by other Components, allowing more confident composed DeFi usage that limits exposure to faults in systems built by others.
As Components become more and more complex, and more Components are composed together as is common in DeFi applications, this gives both users and creators of these apps a powerful tool to eliminate the possibility of many unexpected and expensive outcomes, even when bugs or design flaws are in play.
Creating Radix Components
Creating new Components will use a new, specialized language that we call Scrypto. Scrypto is a functional language, providing a style of programming better suited to defining the kind of FSM-based Components described here. Functional languages are increasingly common, particularly for building reliable high-concurrency systems. Scrypto’s syntax should be familiar to developers who have worked with functional languages, and provide a set of programming primitives particularly suited to creating Component/Action logic.
Put together, we believe the Component model finally provides the purpose-built development environment that DeFi developers need.