From a8bd31af4d8531f4bd0d7d7b088305dc7cab70f8 Mon Sep 17 00:00:00 2001 From: Marcin Chrzanowski Date: Wed, 31 Jul 2019 22:36:24 -0700 Subject: Publish Polymorphism in Solidity post --- src/blog/polymorphism-in-solidity.html | 248 +++++++++++++++++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 src/blog/polymorphism-in-solidity.html (limited to 'src') diff --git a/src/blog/polymorphism-in-solidity.html b/src/blog/polymorphism-in-solidity.html new file mode 100644 index 0000000..a6a6aca --- /dev/null +++ b/src/blog/polymorphism-in-solidity.html @@ -0,0 +1,248 @@ +title: Polymorphism in Solidity +date: July 31, 16:44 +--- +

+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 *);
+    // ...
+};
+
+ + +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. +

-- cgit v1.2.3