title: Polymorphism in Solidity date: July 31, 16:44 --- <p> 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. </p> <p> 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. </p> <p> Let's look at how Linux kernel programmers deal with filesystems. </p> <p> Each filesystem needs its own low-level implementation. At the same time, it would be nice to have the abstract concept of a <em>file</em>, and be able to write generic code that can interact with <em>files</em> living on any sort of filesystem. Sounds like polymorphism. </p> <p> Here's the first few lines of the definition of <a href='https://elixir.bootlin.com/linux/v5.2.4/source/include/linux/fs.h#L922'> <code>struct file</code> </a>, used to keep track of a file in the Linux kernel. <pre> struct file { struct path f_path; struct inode *f_inode; <b>const struct file_operations *f_op</b>; // ... }; </pre> </a> <p> The important bit is the <code>f_op</code> field, of type <code>struct file_operations</code>. Let's look at (an abridged version of) <a href='https://elixir.bootlin.com/linux/v5.2.4/source/include/linux/fs.h#L1791'> <em>its</em> definition </a>. <pre> struct file_operations { struct module *owner; loff_t (*<b>llseek</b>) (struct file *, loff_t, int); ssize_t (*<b>read</b>) (struct file *, char __user *, size_t, loff_t *); ssize_t (*<b>write</b>) (struct file *, const char __user *, size_t, loff_t *); ssize_t (*<b>read_iter</b>) (struct kiocb *, struct iov_iter *); ssize_t (*<b>write_iter</b>) (struct kiocb *, struct iov_iter *); int (*<b>mmap</b>) (struct file *, struct vm_area_struct *); int (*<b>open</b>) (struct inode *, struct file *); // ... }; </pre> </a> It's mostly a bunch of function pointers that appear to be... operations one might want to perform on a file. </p> <p> To emulate OO, inside our "object" (<code>struct file</code>) we manually store a container for its "methods" (<code>struct file_operations</code>). Each of these, as its first argument, takes a pointer to a <code>struct file</code> that it's going to operate on. </p> <p> With this in place, we can now define a generic <a href='https://elixir.bootlin.com/linux/v5.2.4/source/fs/read_write.c#L421'> <code>read</code> </a> system call: <pre> ssize_t __vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos) { if (<b>file->f_op->read</b>) return <b>file->f_op->read(</b>file, buf, count, pos<b>)</b>; else if (<b>file->f_op->read_iter</b>) return new_sync_read(file, buf, count, pos); else return -EINVAL; } </pre> On the other hand, the file <a href='https://elixir.bootlin.com/linux/v5.2.4/source/fs/ext4/file.c#L508'> <code>fs/ext4/file.c</code> </a> defines operations specific to the ext4 file system and a <code>file_operations</code> struct: <pre> const struct file_operations ext4_file_operations = { .llseek = ext4_llseek, .read_iter = ext4_file_read_iter, .write_iter = ext4_file_write_iter, // ... }; </pre> </p> <p> We can do the same thing in Solidity! </p> <p> The example we'll work with is a decentralized exchange. Users can call a <code>trade</code> function with the following signature: <pre> function trade(address sellCurrency, address buyCurrency, uint256 sellAmount); </pre> They specify a currency pair (<code>sellCurrency</code> and <code>buyCurrency</code> - the currency they're selling to and buying from the exchange) and the amount of <code>sellCurrency</code> they're giving to the exchange. The smart contract then calculates the amount of <code>buyCurrency</code> the user should receive and transfers that to them. </p> <p> 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. </p> <p> Here's what a first attempt at implementing this might look like: <pre> // Let address(0) denote Ether function trade(address sellCurrency, address buyCurrency, uint256 sellAmount) { uint256 buyAmount = calculateBuyAmount(sellCurrency, buyCurrency, sellAmount); // <b>take the user's sellCurrency</b> if (sellCurrency == address(0)) { require(msg.value == sellAmount); } else { ERC20(sellCurrency).transferFrom(msg.sender, address(this), sellAmount); } // <b>give the user their new buyCurrency</b> if (buyCurrency == address(0)) { msg.sender.transfer(buyAmount); } else { ERC20(buyCurrency).transfer(msg.sender, buyAmount); } } </pre> </p> <p> This doesn't look terrible yet. </p> <p> Now imagine that you wanted to handle even more asset classes. <p> What if there was a token <code>YourToken</code> that had <code>mint</code> and <code>burn</code> functions callable by the exchange contract? Instead of holding a balance of <code>YourToken</code> you just want to either take tokens out of ciruclation when they're sold, or mint new ones into existence when they're bought. </p> <p> Or you want to support <code>MyToken</code> which I annoyingly implemented without following the ERC20 standard and function names differ from other tokens. </p> <p> With more and more asset classes, the complexity of the code above would increase. </p> <p> Now let's try to implement the same logic but taking inspiration from the Linux kernel's generic handling of files. </p> <p> First, let's declare the struct that will hold a currency's information and methods for interacting with it. This corresponds to <code>struct file</code>: <pre> struct Currency { function (Currency, uint256) take; function (Currency, uint256) give; address currencyAddress; } </pre> </p> <p> Now let's implement taking and giving tokens for two different asset classes. <pre> function <b>ethTake</b>(Currency currencyS, uint256 amount) { require(msg.value == sellAmount); } function <b>ethGive</b>(Currency currencyS, uint256 amount) { msg.sender.transfer(buyAmount); } function <b>erc20Take</b>(Currency currencyS, uint256 amount) { ERC20 token = ERC20(currencyS.currencyAddress); token.transferFrom(msg.sender, address(this), amount); } function <b>erc20Give</b>(Currency currencyS, uint256 amount) { ERC20 token = ERC20(currencyS.currencyAddress); token.transfer(msg.sender, amount); } </pre> </p> <p> Finally, we can perform generic operations on currencies: <pre> function trade(Currency sellCurrency, Currency buyCurrency, uint256 sellAmount ) { uint256 buyAmount = calculateBuyAmount(sellCurrency, buyCurrency, sellAmount); <b>sellCurrency.take(</b>sellCurrency, sellAmount<b>)</b>; <b>buyCurrency.give(</b>buyCurrency, buyAmount<b>)</b>; } </pre> </p> <p> Adding support for a new asset class is now as simple as defining a pair of take/give functions. The code inside of <code>trade</code> need never be touched again, following the Open/Closed principle. </p>