Blog.

Polymorphism in Solidity

Solidity is in many ways similar to C. It's a low-level language, sitting just a thin layer of abstraction above its underlying bytecode. Just like C, it lacks some convenient mechanisms from higher level languages.

There's a cool method for implementing simple object-orientation (complete with polymorphism) in C that can also be applied in Solidity to solve similar problems.

Let's look at how Linux kernel programmers deal with filesystems.

Each filesystem needs its own low-level implementation. At the same time, it would be nice to have the abstract concept of a file, and be able to write generic code that can interact with files living on any sort of filesystem. Sounds like polymorphism.

Here's the first few lines of the definition of struct file , used to keep track of a file in the Linux kernel.

struct file { struct path f_path; struct inode *f_inode; const struct file_operations *f_op; // ... };

The important bit is the f_op field, of type struct file_operations . Let's look at (an abridged version of) its definition .

struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); ssize_t (*read_iter) (struct kiocb *, struct iov_iter *); ssize_t (*write_iter) (struct kiocb *, struct iov_iter *); int (*mmap) (struct file *, struct vm_area_struct *); int (*open) (struct inode *, struct file *); // ... };

To emulate OO, inside our "object" ( struct file ) we manually store a container for its "methods" ( struct file_operations ). Each of these, as its first argument, takes a pointer to a struct file that it's going to operate on.

With this in place, we can now define a generic read system call:

ssize_t __vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos) { if (file->f_op->read) return file->f_op->read(file, buf, count, pos); else if (file->f_op->read_iter) return new_sync_read(file, buf, count, pos); else return -EINVAL; }

fs/ext4/file.c

file_operations

const struct file_operations ext4_file_operations = { .llseek = ext4_llseek, .read_iter = ext4_file_read_iter, .write_iter = ext4_file_write_iter, // ... };

We can do the same thing in Solidity!

The example we'll work with is a decentralized exchange. Users can call a trade function with the following signature:

function trade(address sellCurrency, address buyCurrency, uint256 sellAmount);

sellCurrency

buyCurrency

sellCurrency

buyCurrency

To compilcate things a little, let's say that the exchange deals with more than just ERC20 tokens. Let's allow for ERC20 - ERC20, ERC20 - Ether, and Ether - ERC20 trades.

Here's what a first attempt at implementing this might look like:

// Let address(0) denote Ether function trade(address sellCurrency, address buyCurrency, uint256 sellAmount) { uint256 buyAmount = calculateBuyAmount(sellCurrency, buyCurrency, sellAmount); // take the user's sellCurrency if (sellCurrency == address(0)) { require(msg.value == sellAmount); } else { ERC20(sellCurrency).transferFrom(msg.sender, address(this), sellAmount); } // give the user their new buyCurrency if (buyCurrency == address(0)) { msg.sender.transfer(buyAmount); } else { ERC20(buyCurrency).transfer(msg.sender, buyAmount); } }

This doesn't look terrible yet.

Now imagine that you wanted to handle even more asset classes.

What if there was a token YourToken that had mint and burn functions callable by the exchange contract? Instead of holding a balance of YourToken you just want to either take tokens out of ciruclation when they're sold, or mint new ones into existence when they're bought.

Or you want to support MyToken which I annoyingly implemented without following the ERC20 standard and function names differ from other tokens.

With more and more asset classes, the complexity of the code above would increase.

Now let's try to implement the same logic but taking inspiration from the Linux kernel's generic handling of files.

First, let's declare the struct that will hold a currency's information and methods for interacting with it. This corresponds to struct file :

struct Currency { function (Currency, uint256) take; function (Currency, uint256) give; address currencyAddress; }

Now let's implement taking and giving tokens for two different asset classes.

function ethTake(Currency currencyS, uint256 amount) { require(msg.value == sellAmount); } function ethGive(Currency currencyS, uint256 amount) { msg.sender.transfer(buyAmount); } function erc20Take(Currency currencyS, uint256 amount) { ERC20 token = ERC20(currencyS.currencyAddress); token.transferFrom(msg.sender, address(this), amount); } function erc20Give(Currency currencyS, uint256 amount) { ERC20 token = ERC20(currencyS.currencyAddress); token.transfer(msg.sender, amount); }

Finally, we can perform generic operations on currencies:

function trade(Currency sellCurrency, Currency buyCurrency, uint256 sellAmount ) { uint256 buyAmount = calculateBuyAmount(sellCurrency, buyCurrency, sellAmount); sellCurrency.take(sellCurrency, sellAmount); buyCurrency.give(buyCurrency, buyAmount); }