exeDAO is a configurable multi-signature framework that enables secure smart contract extensibility. By restricting opcodes that can modify contract storage, exeDAO can securely execute near-arbitrary code without compromising the integrity of the DAO.
We prototyped this concept at EthNewYork 2019 and are open-sourcing the improved contracts with new consensus, share distribution, and extensibility functions, as well as a demoable UI.
When we went to ETHNY in May, we wanted to make a DAO which could execute arbitrary bytecode scripts without the risk of compromising share ownership; that is, we wanted the mechanism determining how we approve code to run to be secured from the code being run.
The initial contract was very simple — you deploy the contract with a list of DAO members and the number of those members who must approve any arbitrary execution.
To run a script, you create a contract with two functions: a
kill() function which only calls and a fallback function which executes the script you want to call. When the contract is compiled, you send it to the DAO.
To make sure the provided script does not modify share ownership, the contract ensures that no
delegatecall opcodes are included in the contract’s bytecode. We restricted
delegatecall because it could potentially bypass our
Once the bytecode is checked for potential storage modification, the DAO deploys the code,
delegatecalls the new contract (executing the script), destroys it by calling kill() and finishes. By calling
kill() at the end of the script execution, the caller gets to save about 16k gas.
Inspirations for a new framework
We wanted to integrate three core ideas into one framework for the exeDAO v1 release.
On-chain bytecode auditing
The bytecode auditing idea came from putting together the following basic facts:
- Some contracts on Ethereum need to have the ability to upgrade their functionality, but doing so can introduce security issues.
- The logic of Ethereum contracts is defined in their bytecode, which consists of constant values and assembly functions.
- Contracts can read each other’s bytecode.
By looking through the bytecode of a contract on-chain, you can check which assembly functions it uses; therefore, if you define the functions you don’t want extensions to use, you can restrict the capabilities of future upgrades by whatever constraints are reasonable for a given application.
Adding extensions to smart contracts to enhance functionality without redeployment is an immense benefit to developers, as long as it is done safely. Existing frameworks like Gnosis and Aragon have built-in extensibility through the use of modules, but we wanted to integrate the bytecode auditing functionality into this process to prevent extensions from modifying storage.
While we chose to restrict the opcodes that could lead to self-destruction or storage modification, the same process could be used for any kind of restriction.
Granular approval thresholds
Existing DAOs tend to have simple permission systems: if a threshold of voters approves some proposal, it is approved. The threshold is usually a constant percentage or number of voters, and it is usually the same for every function.
We wanted to make a contract where each function has its own approval threshold which is entirely configurable by the DAO members. Since every function call to an Ethereum smart contract begins with a function signature, this was pretty simple to implement: map function signatures to approval requirements, make a function to check the message signature of the current function call against an approval requirement and a function to change the requirements and bada bing bada boom, you’ve got a configurable approval mechanism that works for functions in the base contract as well as extensions.
For our system, approval thresholds are integers from 0–255, where:
- 0 = Not set, can not be approved
- 1–100 = Percentage of shares which must approve a proposal
- 255 = Can be called by anyone with an Ethereum address
For exeDAO v1 we have made some modifications to the original bytecode audit/execution methods, added configurable permissions, shares management (as opposed to a static list of contract owners and a constant threshold) and extension capabilities.
exeDAO provides two methods for extending functionality beyond the base methods included in the contract:
safeExecute takes some bytecode for a contract, checks it for dangerous opcodes, deploys the bytecode as a new contract,
delegatecalls into it with no input arguments, and returns the result. safeExecute is essentially exeDAO’s
eval() method. It runs whatever code you give it, allowing daoists to perform one-time script execution within the context of the contract. This is essentially the same as the method used in the original exeDAO, with one exception: we removed the
kill() call and added
selfdestruct to the list of restricted opcodes. The risk of destroying the entire contract seemed a bit more substantial than the benefit of saving 16k gas.
addExtension works a little differently. It takes in bytecode for an extension contract, a list of function signatures that are present in it and a boolean stating whether the extension should be used with
delegatecall. The bytecode given is deployed to a new contract whose address is saved with the other input data (except the bytecode) as an
Extension struct. This extension is saved in an array so users can quickly look up all extensions, and the function signatures are mapped to the extension’s index.
Whenever a user calls a method on exeDAO which does not exist in the base contract, the fallback function checks if the function signature of that data is mapped to an extension. If it is, the same approval mechanism as is used for every other function on the contract determines whether the execution can move forward, then we pass the call data to the extension contract in a
call, according to the extension settings.
Since the function calls to extension contracts are done through the fallback method rather than some type of
callExtension() function, all external contracts and APIs can treat the contract as if it had all the extension functions originally.
Extensions are expected to contain a
metaHash value which is used to look up the extension’s metadata on IPFS. exedao.js expects extension metadata to contain:
abi— ABI of the extension contract
bytecode— bytecode of the compiled contract
functionDescriptions— mapping from function signatures to brief descriptions of what they do
ExeDAO.sol contract. The library is capable of looking up extensions on the contract, finding their metadata on IPFS and upgrading its contract’s ABI with the ABI from the extensions.
Proposals to execute code on the contract are identified by a hash of the full call data to whichever function the proposal targets. Proposal hashes are mapped to indices in an array of
Proposal structs, which contain the vote count and mapping of who has voted. When a proposal is approved or expires, the hash-to-index mapping entry is deleted along with the non-mapping values in the struct. By mapping to indices in the array rather than directly to the structs, the same proposals can be executed multiple times without needing to clear out potentially large voter mappings.
Proposals can optionally include a
metaHash value which can be used to look up metadata about a proposal on IPFS. The metadata can be used to share any information about proposals, as the contract has no mechanism for verifying metadata; in exedao.js, metadata is expected to have:
title— name for the proposal (optional)
description— summary of the proposal (optional)
function— function signature of the method being called (required)
arguments— array of function parameters (required)
This metadata allows clients to query the list of open proposals, retrieve their metadata from IPFS, verify that the function and inputs for each proposal match the proposal hash on-chain, and display the function and arguments to users.
exedao.com has a web server that allows any DAO member to submit a metadata object for a proposal. If the metadata is verified to match an open proposal and has not already been saved, the server will pin the metadata to IPFS by using the Temporal API. The server is not necessary — any method of uploading data to IPFS is suitable (or really any method of sharing data), but it provides a simple method for DAO members to share metadata about proposals.
The server also allows members to upload “members-only” proposal metadata which is stored on the server (not uploaded to IPFS) and can only be accessed by DAO members. This is a useful feature as it allows the DAO members to agree on a proposal without alerting third parties as to what the contract is about to execute. A better method might be to use a shared key derivation mechanism so DAO members can encrypt metadata, upload it to IPFS, retrieve and decrypt the data. We’ll be looking into this in the future.
The authentication middleware is pretty useful on its own. It uses web3 signatures to verify that users are members of the DAO before issuing JWTs. With some minor modifications, this can be extended for use with any web server where users authenticate with web3 signatures after some on-chain data about their addresses is verified (has a valid ens subdomain, for example). The middleware works with both express and socket.io, though only express is used by exeDAO at the moment.
There are a number of things we still want to accomplish with exeDAO.
One of our top priorities is writing some documentation for the contracts and the JS library. We wrote a lot of comments but we want to make it easier for developers to understand and play with our code. In the meantime, we’re available on Discord to answer any questions you have.
The UI for exeDAO needs some love — the extensions might be better handled as separate UI components rather than being included in the proposal form dropdown and there are a few other changes the app could use.
We’re working on a package registry that includes a lot of the functionality from exeDAO to make it easier for people to use the on-chain bytecode auditing without needing to inherit our contracts.
Buidl with us!
Hop on our Discord!
There’s a lot in these contracts we weren’t able to get to, but we’re usually on Discord so stop by if you want to make small talk about assembly or ask about any of the code.
0age for providing advice through the development process,
Mudit Gupta for pointing out that
delegatecall was a necessary restriction during the hackathon,
Paul Psyioc for the awesome logo,
Temporal for the simple IPFS pinning,
and the EthNewYork judges for selecting us as finalists.