I believe that Solidity would benefit from first class support that models realistic deployment scenarios for EVM contracts. For a variety of reasons, not the least of which is the size constraint on contracts, complex projects end up releasing multiple contracts that are closely affiliated with each other, and/or which together compose a whole. Unless I am misunderstanding the language, or am uninformed about upcoming features, I believe these issues are not well supported in the existing and/or planned language.
Rather than present a wholly formed set of language proposals, I wanted to state this problem and get some conversation going. Yes, I am of course aware that openzep and other projects have “proxy” contracts etc but I (flame suit on) would generally categorize all of these are cobbled together hacks on top of less than ideal language support.
Speaking philosophically, the target EVM and its associated blockchain tools together with solc compose the complete execution environment and should ideally be more completely integrated, so that realizing a functioning holistic contract system has a smaller error surface than is currently experienced by developers and maintainers. A microcosm of these issues is evident in how external libraries are handled and how errors can occur therein.
There are a number of frustrations that come up when attempting to use solidity 0.8.20 to write, deploy and maintain a closely cooperating set of contracts with the intention of offering a composed interface through a single storage address while also trying to do so with minimal gas costs.
Again, I could take the approach of writing a long paper on this topic but that would delay getting the conversation started. So instead, let me offer a small example. Lets pretend for a minute that I want to define an interface Iface
which contains three functions fA
, fB
, and fC
but each of those three functions is so large that it cannot be deployed to the same address. Remember, this is just a contrived example, to discuss the issues. In practice, Iface would have tens of smaller functions which as a group are too large to deploy to a single address.
For devops and language reasons, I end up wanting to declare a storage contract parent CStorage
that defines all the storage vars of and which is
the Iface
. But now, in order to deploy, I must somehow create three derived contracts into which I put one of the three functions above. Since those contracts do not define the other functions, they must be declared abstract. If they are abstract, no code will be emitted for them. If they define stubs for the other functions, then delegatecalls to their addresses made to those functions will succeed when they arguably should fail. If the stubs did not exist, and the delegatecalls failed, it would be possible to write a proxy that (granted, for more gas) could try each available deployment address until one succeeded. While that sounds expensive, consider public view functions which in practice cost no gas because they are not executed on-chain. Under the current system, we have to register function selectors with the proxy and pay the cost of a map lookup for every call in order to successfully dispatch to the correct implementation address. Since function selectors MUST be keccak hashes, there is no way to write a proxy dispatcher which (for example) is instead based on looking at the first byte of the selector or function name (i.e., deterministic precomputed dispatch is impossible). Furthermore, the registration procedure is “outside the language” and error prone. The language offers no way for a programmer to declare that contracts A,B,C together implement Iface, in parts and therefore is not helpful in ensuring that Iface is correctly distributed across those implementation artifacts.
I have many ideas that address these various issues. This is by no means an exhaustive list of the various issues that need to be addressed. Another one, for example, is that I believe there is no way to (without diving into assembly) make a delegatecall through a contract address. External libraries are not enough, as these do not have first class access to storage of the caller without polluting their parameters with storage pointers, which just means that developers are tempted to put all of a contract’s variables into a struct … which is precisely what the storage vars of a contract are (thing this
pointer in other languages).
I hope the foregoing is sufficient to start a conversation. I am not conversant with the current priorities of the solidity devs, but unless non of this will matter soon because a WASM evm is around the corner and size and other existing limitations are about to be tossed out the window (i.e., major changes to the execution context are imminent), these issues seem like they should be close to top priority for the language.
I appreciate that I am not an expert in everything solidity and do understand that I may just have it all wrong. Again, this is just meant to start a conversation and share some of my thoughts/experience after working on a large complex project using the available tools.