On Ethereum developer experience
My notes on the Ethereum developer ecosystem and tooling
This week I wrote a simple TODO dApp to explore the Ethereum developer ecosystem. It works like this: anyone can submit a task with a bounty, and anyone can claim they performed it. The author of the task then picks one of the submissions, and the submitter gets the bounty.
Here is how it looks:
Github repo is here (I cut a bunch of corners on the client side and chain side; if you use this code, you’ve been warned). Tooling is hardhat, next.js, wagmi/ethers, RainbowKit, typescript, and I think that’s pretty much it.
Here are my notes on the developer ecosystem and tooling:
The chain side is far ahead of the front-end side in terms of developer experience. I’m not sure how much of the client side complexity is essential and how much is incidental. I suspect much can be improved. To give you an idea of the time breakdown, it took me about an hour to write the Solidity contract, and about two days to write the front-end.
Solidity + hardhat are great. I’m a PL nerd so not a huge fan of Vitalik’s decisions on Solidity design, he borrowed too much from C++/Java, and not enough from everywhere else. But it works, it’s easy to learn, and worse is better. And if I’m so smart, why don’t I write a better programming language that compiles to EVM anyway? (I might!)
The one serious area of concern with Solidity is security. It’s simply too easy to introduce security bugs, and much of this is incidental. At a minimum you have to go down the list of common Solidity security vulnerabilities and carefully check every line of your code against that list. I think large classes of these vulnerabilities could be eliminated either through different programming language design, better static analysis tools, good fuzzers, or, more likely, all of the above. This couldn’t have been determined a-priori and needed to be discovered through experience. Now that enough experience has been accumulated, I think there is an opportunity for designs that are considerably more secure by default.
Area of improvement: tooling to eliminate large classes of Solidity security vulnerabilities.
While we’re on Solidity, this isn’t a huge deal for simple contracts, but as contracts get more complex the change-recompile-deploy loop becomes suboptimal. I’d love to see a version of the toolchain that doesn’t require redeploying with every change (and blowing away the living environment in the process).
Area of improvement: a Solidity/EVM repl.
Most of the frustration originated from getting everything to work on the client. There isn’t one big thing— it’s a case of one thousand papercuts. Reality always has a surprising amount of detail, and this is especially true of Ethereum clients reality.
For example, ethers.js is well documented, but only if you already know how to use ethers.js. If you don’t, you copy-paste some code and hope for the best. As a pop quiz, suppose you create an ethers Alchemy provider, connect your dApp to the user’s Metamask wallet, and switch the wallet to talk to your localhost hardhat node. Does Alchemy then somehow talk to my local blockchain instance? Does ethers switch away from Alchemy automatically? Something else? And what precisely does “connect to a wallet” mean? You can cobble together code that works, but these are pretty fundamental questions that you can’t answer without a considerable time investment.
Area of improvement: a much better FAQ for questions that everyone who develops on Ethereum quickly forgets are questions because of the curse of knowledge.
Currently there is no commonly adopted tooling that makes contract calls from the client typechecked. You pass ethers the contract function name you’re trying to call as a string, pass the arguments as an array, obviously make a few mistakes, and then repeatedly fix them until the call works. This comes with all the usual issues— if the contract changes you don’t automatically find out your client is now wrong, no autocomplete, etc.
In general, error handling on the client is really tough. Almost everything related to Ethereum calls can fail at any time in unpredictable ways, so you have to spend a lot of time defensively coding around it.
Area of improvement: automatically generate typescript clients for given contracts.
To make everything work with React you can write your own hooks, caching layer, etc., or you can use wagmi. You can think of Wagmi as swr or React Query, but for Ethereum contracts instead of REST calls. Wagmi is really nice because it makes your UI much more responsive, but can be quite confusing to use for incidental reasons. For example, it accepts the contract ABI generated by Hardhat, but if you wrap that ABI in an ethers Interface and pass that, it type checks but seems to throw weird errors. It also just throws weird errors at runtime in general (see here, here, and here— even though the issues, among many others, are closed I encountered each of these errors and spent maybe 3-4 hours chasing them down).
Another problem is that wagmi doesn’t support conditional fetching, which means that if you need to make a contract call whose arguments depend on a previous contract call, you can’t without putting the hooks in different React components.
Incidentally, if you connect wagmi to your hardhat node
useContractReads won’t work because wagmi expects a
Multicall contract to be deployed, but if you deploy the one you find on Google it still won’t work because it needs an obscure
Multicall3 contract that took me an hour to find. (In hindsight I should have forked my local node off mainnet so I wouldn’t have to deal with this problem.)
Area of improvement: send the wagmi team a boatload of PRs improving documentation and fixing the common failure modes.
Speculative: data access
The chain is really good at doing its chain thing: maintaining a permanent record of decentralized trusted transactions, but that usually isn’t the best way to access data and build apps. For example a common Solidity pattern to store an arbitrarily long list of records is to have a mapping from integers to data, a member variable with the number of records, and increment an integer every time you want to insert something new. See here and here. In the TODO dApp I do this twice— once to store the tasks, and once on each task to store the claims of performed work.
Unfortunately on the client it means that getting the list of tasks involves first reading the integer from the chain, and then making a call for each integer from zero to the size (don’t forget the off by one errors!)— see here and here. That’s painful in a simple toy dApp, and unworkable in a real dApp of any meaningful scale. You can’t filter, sort, join, or do any of the things app developers have been doing since apps became a thing.
This suggests there is a good chance future dApps won’t be reading directly from the chain. One plausible path is that the chain will do its permanent distributed trust magic, there will be a layer on top that indexes and transforms the data into a form usable by dApps, and dApp developers will write code to read from this layer. An existing version of this is The Graph which is brilliant, but somewhat too heavyweight and idealistic for this use case. A friend pointed me to Goldsky, something that looks much more tractable, supports realtime use cases, but is still in a closed beta.
This would still make dApp development difficult— I can easily imagine writing a transformer for a mature contract, but developing contracts, transformers, and frontend apps simultaneously would be quite painful. This seems like a fruitful area of research to me. One angle of attack is to imagine what this looks like in five years, and work backward from there. (I might write more about this in the future, but if you’re working on this problem shoot me an email!)
Area of improvement: decouple contract data layout from dApp data access.
Much to be done
To sum up, here are all the possible areas of improvement I’ve run into so far:
tooling to eliminate large classes of Solidity security vulnerabilities
a Solidity/EVM repl
a much better FAQ for common questions (and better docs in general)
automatic generation of typescript contract clients
wagmi library polish
decouple contract data layout from dApp data access
Thanks for reading Zero Credibility! Subscribe for free to receive new posts and support my work.
Hey Slava! By far & wide agree with your analysis, the client-side is a source of immense pain.
Some suggestions for your wishlist:
- typescript generation: I believe typechain does this (but I felt some pain when I used it)
- REPL: doesn't exist that I know of, but Solidity scripting in Foundry comes close to what I'd like to use a REPL for
(In general, Foundry is a step up from Hardhat - which is already pretty good. The two killer features are compilation speed & being able to write tests directly in Solidity)
- tools for finding Solidity bugs: multiple things like this exist though they could probably improve. The one that is top of mind is Slither. There are also a few fuzzers like Echidna.
Finally, a small correction: I don't think Vitalik was involved with Solidity. I think Gavin Wood might have come up with the initial design & then it was developped by a dedicated team (Wikipedia names Christian Reitwiessner & Alex Beregszaszi).
Have you heard of Clarity? https://clarity-lang.org/ or https://docs.stacks.co/docs/write-smart-contracts/
Clarity is a non-Turing complete/decidable, interpreted smart contract language used primarily on Stacks, a smart contract abstraction layer on Bitcoin.
It's a LISP-like language and imposes an explicit and opinionated view on smart contract development. There is a much smaller developer ecosystem—like a few hundred compared to the ~20,000 in all of crypto—so the network effect isn't comparable to the EVM, although Stacks will soon be extended with an L2, Subnets, that can be EVM-compatible.
I work for Hiro, the dev tooling company, so I'm biased. But I've been thinking a lot about the 1) network effects of EVM + Solidity versus 2) its attack, exploit, and bug surface, and wondering what will win out.