Vincent A Saulys' Blog
Test Driven Development in DeFi & Crypto (part 1)
Tags: crypto defi
September 27, 2021

One of the core issues of the blockchain is that its immutable -- that code will stay there forever.

That simple fact means you need to spend time on making sure your code works and doesn't

How much does this really matter?

The inspiration for this article come from the now infamous TITAN equality bug. To make a very long story short, IRON, a dollar stablecoin, uses a sister-crypto-as-collateral called TITAN in its minting process. This TITAN currency went to zero and now a bug in IRON is preventing people from redeeming their tokens for US dollars.

Image of the bug in question in TITAN

Link to the source code in question. If you're curious, you can read more about it here.

A fix cannot be deployed easily owing to how cryptocurrencies work. Code is immutable on the blockchain and once deployed cannot be changed. This "off-by-one" error -- it should've been a greater-than-or-equal rather than a greater-than -- literally cannot be corrected.

How can We Avoid This?

The problem is that people pursue tactical programming when they should be pursuing strategic programming.

Tactical Programming is coding as you think until you get what "looks" right. It's best exemplified by hackathons. You have an idea that you want to MVP. You build until you get it out the door. This gives you a tangible thing you can show to people.

Strategic Programming has software design go into your work. Typically this comes in the form of a technical spec -- more on this later -- which defines what you intend to build and how it should be organized in the source code.

Tactical Programming is popular with startups and hackathons. These environments involve a lot of experimentation and the desire to "move fast." Once you have a solid idea of what your product should look like, you should immediately move to tactical programming. John Osterhout has written extensively about this in his wonderful "Philsophy of Software Design", which comes highly recommended.

That's because tactical programming means you have to think out what you'll build and what each part will do. It makes you think through edge cases, work from the customer's perspective, and know what the intended behavior of a program will be.

The best way to approach stragetic programming is by composing a technical spec (sometimes called a functiona spec). There are many ways to do this and many older guides already exist but the core points are the same:

  1. Who is your end user?
  2. How will they use the product?
  3. What will your product do? What won't it do?

After writing these you then write tests that are coded examples of what you mean.

For instance, let's take a function that determines if a supplied number is a prime or not. We may write a test for the user who wants to pass in a float value or null value, something that can't be classified as prime. When we go to write out tests, we'll have to think about that expected behavior: should the function throw an error? return a -1? return a false value? This forces us to think through what we intend to get out of the function before we write it. Otherwise, we may begin programming only to need a radical rewrite to accomodate this usage.

But why TDD in Crypto and DeFi?

DeFi (decentralized finance) uses programmable money to build financial products. Your ethereum gets locked into decentralized exchanges, liquidity providers, and other mechanisms. Knowing how these mechanisms will interact and work is vital.

The example of TITAN & IRON above proves just how vital this is. Nobody thought through what would happen if the accompanying crypto went to zero. This caused a "greater-than" sign to be inserted when it should've been a "greater-than-or-equal." Unlike web development, its a really hard to fix bug. These are the sorts of events you should think through and put them into a technical spec.

What will be building?

In this tutorial, we'll apply these techniques to cryptocurrencies and decentralized finance. We'll use the following order to determine our coding:

  1. Write a technical spec
  2. Program out tests that many this technical spec concrete and tangible
  3. Code the smart contract that makes these tests pass

For our technical spec, we won't actually be writing one but instead will use the ERC20 as a "gold standard" as an example. In most "real life cases," you'll use a pre-built ERC20 library like OpenZeppelin or Consensys but this will work for this tutorial.

How is a Crypto Technical Spec structured?

They all follow the format:

  1. Summary
  2. Abstract
  3. Motivation
  4. Specification (Methods + Events)
  5. Errata & Notes

Below is a sample taken from the markdown for the ERC20 standard:

## Simple Summary

A standard interface for tokens.

## Abstract

The following standard allows for the implementation of a standard API
for tokens within smart contracts. 
This standard provides basic functionality to transfer tokens, as well
as allow tokens to be approved so they can be spent by another
on-chain third party. 

## Motivation

A standard interface allows any tokens on Ethereum to be re-used by 
other applications: from wallets to decentralized exchanges. 

Right off the bat, we know the What & the Why of the paper. This lets people who are reading through papers to glance at the abstract and know if this is the right spec for them.

The Summary is a one line description of your spec. Think of it as a 30s elevator pitch. The Abstract is a big longer. It's for those intrigued enough to read on. Both keep it in plain english.

The Motivation desribes the Why -- "Why must this be built? What is the benefit to the end user?" As most crypto projects are meant for other technical users, these tend to think in engineering terms like interfaces.

What goes into the Technical Spec?

Next we get to the description of events and methods. Each method and event is described in painful detail with all the caveats you should be aware of. Those descriptions will become the basis of our tests.

A sidebar: ERC721 arguably does this better because it bakes it into a working interface.

We'll take those methods and event names and bake them into an interface, laid out below:

pragma solidity =0.8.0;

// note that I added separating lines to differentiate no wallet, one
// wallet, and multiple wallet functions -- more on that in part 2

interface IERC20 {

    event Transfer(address indexed _from, address indexed _to, uint256 _value);
    event Approval(address indexed _owner, address indexed _spender, uint256 _value);

    function name() external view returns (string memory);
    function symbol() external view returns (string memory);
    function decimals() external view returns (uint8);

    function balanceOf(address _owner) external view returns (uint256 balance);
    function totalSupply() external view returns (uint256);

    function approve(address _spender, uint256 _value) external returns (bool success);
    function allowance(address _owner, address _spender) external view returns (uint256);
    function transfer(address _to, uint256 _value) external returns (bool success);
    function transferFrom(address _from, address _to, uint256 _value) external returns (bool success);

Now, at a glance, we can see which methods need to be build and why. We can take this and use it in our actual smart contract too.

Technical Walkthrough Or, How do I build this thing?

See more about that in the next part

Share On: