m-chrzan.xyz
aboutsummaryrefslogtreecommitdiff
path: root/src/blog/polymorphism-in-solidity.html
blob: a6a6acac5f08dbf809d23105bd72f602229ac3e7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
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-&gt;f_op-&gt;read</b>)
        return <b>file-&gt;f_op-&gt;read(</b>file, buf, count, pos<b>)</b>;
    else if (<b>file-&gt;f_op-&gt;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>