From a8bd31af4d8531f4bd0d7d7b088305dc7cab70f8 Mon Sep 17 00:00:00 2001
From: Marcin Chrzanowski
+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
+
+
+The important bit is the 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;
+ // ...
+};
+
+
+
+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 *);
+ // ...
+};
+
+
+
+It's mostly a bunch of function pointers that appear to be... operations one
+might want to perform on a 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; +} ++ +On the other hand, the file + +
fs/ext4/file.c
+ defines operations specific to the ext4 file system and a
+file_operations
struct:
+
++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); ++ +They specify a currency pair (
sellCurrency
and buyCurrency
-
+the currency they're selling to and buying from the exchange) and the amount of
+sellCurrency
they're giving to the exchange. The smart contract then
+calculates the amount of buyCurrency
the user should receive and
+transfers that to them.
+
+
++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); +} ++ + +
+Adding support for a new asset class is now as simple as defining a pair of take/give
+functions. The code inside of trade
need never be touched again,
+following the Open/Closed principle.
+