|
| 1 | +--- |
| 2 | +author: "cydonia" |
| 3 | +date: "2024-02-03" |
| 4 | +labels: ["cydonia", "rust"] |
| 5 | +description: "Thoughts about the storage implementation in zink." |
| 6 | +title: "Thoughts of ERC-20 in Zink" |
| 7 | +--- |
| 8 | + |
| 9 | +Have been stuck in the development of zink for around 2 months, feeling my life is worthless though these days since |
| 10 | +I haven't done anything that I'm feeling proud of. |
| 11 | + |
| 12 | +The ERC20 implementation in zink is a big picture which leads zink to a real programming language of EVM when it gets |
| 13 | +completed, I'm too scared to see that something **solidity** can do but **zink** can not that I could not even push one |
| 14 | +more commit in zink these days. |
| 15 | + |
| 16 | +## Splitting Problems |
| 17 | + |
| 18 | +Another night without sleeping, tired of video games again finally, get up and check the code base of zink, I found that |
| 19 | +I have already cleaned the code structure of the code generation module last time which makes it easier to catch up my |
| 20 | +previous work this time. |
| 21 | + |
| 22 | +What if I just start my work on ERC20, splitting the problems out into issues like always, the layout of [ERC20][erc20] is |
| 23 | +pretty simple, ERCs in zink will be implemented in rust traits without doubts. |
| 24 | + |
| 25 | +```solidity |
| 26 | +abstract contract ERC20 is Context, IERC20, IERC20Metadata, IERC20Errors { |
| 27 | + mapping(address account => uint256) private _balances; |
| 28 | + |
| 29 | + // ... |
| 30 | + |
| 31 | + /** |
| 32 | + * @dev Sets the values for {name} and {symbol}. |
| 33 | + * |
| 34 | + * All two of these values are immutable: they can only be set once during |
| 35 | + * construction. |
| 36 | + */ |
| 37 | + constructor(string memory name_, string memory symbol_) { |
| 38 | + _name = name_; |
| 39 | + _symbol = symbol_; |
| 40 | + } |
| 41 | +} |
| 42 | +``` |
| 43 | + |
| 44 | + |
| 45 | +## Storage |
| 46 | + |
| 47 | +Mapping storage is missing in zink for now bcz I haven't got a perfect idea for passing bytes from rust to evm yet. |
| 48 | + |
| 49 | +```solidity |
| 50 | +mapping(address account => uint256) private _balances; |
| 51 | +``` |
| 52 | + |
| 53 | +But for doing it in hard way, I have already got an idea from the design of my old friend [ink][ink], for the [Mapping][ink-mapping] |
| 54 | +storage implementation in **ink**, they are mapping pairs into storage slots, mb it doesn't look clever but it seems that this is |
| 55 | +the best solution anyway. |
| 56 | + |
| 57 | +```rust |
| 58 | +pub struct Mapping<K, V: Packed, KeyType: StorageKey = AutoKey> { |
| 59 | + #[allow(clippy::type_complexity)] |
| 60 | + _marker: PhantomData<fn() -> (K, V, KeyType)>, |
| 61 | +} |
| 62 | +``` |
| 63 | + |
| 64 | +I was actually struggling with how to use rust's **BTreeMap** in zink before, using **BTreeMap** is friendly to |
| 65 | +rust developers because we are familiar with it, however there are two big problems in it: |
| 66 | + |
| 67 | +- The methods of **BTreeMap** or any **Iterator** requires allocation from memory which will embed tons of code |
| 68 | +to support it. |
| 69 | +- Even if we are okay with the tons of allocation code, store a encoded **BtreeMap** in evm takes a lot anyway, |
| 70 | +bcz we need to handle encoding/decoding stuffs which burns gas a lot. |
| 71 | + |
| 72 | +In conclusion, due to the two points above, it is very unfortunate that using Rust's **BtreeMap** in zink is not |
| 73 | +a proper solution. |
| 74 | + |
| 75 | +### Store `bytes` in zink! |
| 76 | + |
| 77 | +Zink currently only support `i32` in storage, bcz I haven't got a solution passing bytes elegantly yet, but for moving forward |
| 78 | +to the goal of ERC20, I compromise to implement it anyway, the current solution requires an `Abi` trait _(I hate the naming `Abi`, |
| 79 | +because it can describe too many interfaces in zink xd)_. |
| 80 | + |
| 81 | +```rust |
| 82 | +pub trait Abi { |
| 83 | + fn write(&self) -> Result<()>; |
| 84 | +} |
| 85 | + |
| 86 | +// ... impl Abi for number types |
| 87 | +impl Abi for i32 {} |
| 88 | + |
| 89 | +// ... impl Abi for bytes |
| 90 | +impl Abi for [u8; 0] {} |
| 91 | +impl Abi for [u8; 1] {} |
| 92 | +// ... |
| 93 | +impl Abi for [u8; 32] {} |
| 94 | +``` |
| 95 | + |
| 96 | +`sstore` requires two stack inputs as key and value, for simplifying the problem, for the types which implements `Abi`, we only need to |
| 97 | +make sure that they can be transformed into bytes under the length 32 at the moment. |
| 98 | + |
| 99 | +```yul |
| 100 | +PUSH0 // key |
| 101 | +PUSH0 // value |
| 102 | +SSTORE |
| 103 | +``` |
| 104 | + |
| 105 | +Thus we need generate matched FFI for these stuffs as well, which is closed to the assembly implementation, |
| 106 | + |
| 107 | +```rust |
| 108 | +mod ffi { |
| 109 | + fn store_key_i32_value_i32(key: i32, value: i32) {} |
| 110 | + fn store_key_i32_value_bytes(key: i32, ptr: i32, length: i32) {} |
| 111 | + // ... everything |
| 112 | +} |
| 113 | +``` |
| 114 | + |
| 115 | +I love writing macros in rust but not the generated code like above looks really ugly, but seems I have to do it now anyway. |
| 116 | + |
| 117 | + |
| 118 | +### Store maps in zink |
| 119 | + |
| 120 | +I don't like the naming `mapping` because it is too long, so if I can choose, I'll use `map` because it is shorter, typescript is using `Map` |
| 121 | +or `Record` as well, so I don't understand why solidity is using the keyword `mapping`, mb because they have token `=>` in the storage declaration, |
| 122 | +and they want to express **ing**. |
| 123 | + |
| 124 | +So for `Map` in zink, it will follow the well-designed `Mapping` in `ink`, provided `Key`, `Value`, and `Prefix`, the problem will be concatenating |
| 125 | +the storage key in zink. |
| 126 | + |
| 127 | +And the solution is using macro, again: |
| 128 | + |
| 129 | +```rust |
| 130 | +mod ffi { |
| 131 | + fn store_map_key_i32_value_i32(prefix: i32, key: i32, value: i32) {} |
| 132 | + fn store_map_key_i32_value_bytes(prefix: i32, key: i32, ptr: i32, length: i32) {} |
| 133 | +} |
| 134 | +``` |
| 135 | +However, we can fix `i32` as prefix this time, because the storage keys of a contract may never reach the max limit of `i32`. |
| 136 | + |
| 137 | +## Interfaces |
| 138 | + |
| 139 | +After solving the storage problem, the next one is the design of `interface` in zink, like mentioned above, we can use trait without doubts, but the |
| 140 | +problem is that we need to export the methods provided by traits to WASM as well, hmm, this problem leads us to a derive macro, |
| 141 | + |
| 142 | +```rust |
| 143 | + |
| 144 | +#[derive(Erc20)] |
| 145 | +struct MyContract {} |
| 146 | + |
| 147 | +// Which generates |
| 148 | + |
| 149 | +#[no_mangle] |
| 150 | +extern "C" fn total_supply() {} |
| 151 | +``` |
| 152 | + |
| 153 | +Looks weird, but it works, should zink ask users to use `MyContract` to define the namespace is a problem as well, maybe we can provide different |
| 154 | +solutions for this first, but as the experience from apple, we'd better only give users the best solution finally at this kind of points. |
| 155 | + |
| 156 | +## Errors and Events |
| 157 | + |
| 158 | +Errors and events are about to be refactored as well since now we have a solution for passing bytes in zink now, even it is ugly, but it is best |
| 159 | +solution for now )) |
| 160 | + |
| 161 | +[ink]: https://github.com/paritytech/ink |
| 162 | +[ink-mapping]: https://docs.rs/ink/latest/ink/storage/struct.Mapping.html |
| 163 | +[erc20]: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol |
0 commit comments