diff options
author | Marcin Chrzanowski <marcin.j.chrzanowski@gmail.com> | 2019-07-31 22:36:24 -0700 |
---|---|---|
committer | Marcin Chrzanowski <marcin.j.chrzanowski@gmail.com> | 2019-07-31 22:36:24 -0700 |
commit | a8bd31af4d8531f4bd0d7d7b088305dc7cab70f8 (patch) | |
tree | 010d35a45a038ab7e5e4236179c94e5ec7daff84 /src/blog/polymorphism-in-solidity.html | |
parent | 77084cd3f4567f923ed236ddb273613e8e5c3397 (diff) |
Publish Polymorphism in Solidity post
Diffstat (limited to 'src/blog/polymorphism-in-solidity.html')
-rw-r--r-- | src/blog/polymorphism-in-solidity.html | 248 |
1 files changed, 248 insertions, 0 deletions
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 +--- +<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> |