Commentary on the recent DAO audit
About two years ago when API3 had just been founded, we had a generic Aragon DAO as a placeholder (referred to as DAOv1). This needed to be replaced with a tailored implementation that implements features that facilitate API3 tokenomics such as staking and the resulting pool being utilized as collateral for service coverage.
At the time, I was cautiously hoping for the core technical team to be able to delegate the development of the DAO and its front end to a dedicated team. This approach wasn’t too successful, which forced the core technical team and ChainAPI to undertake the task under extreme time pressure. The market conditions at the time caused some auditors to overbook and fail to honor their dates. As a result, our last two audits, the ones from Quantstamp and Team Omega, had to be done in parallel (ideally they would have been sequential). Nevertheless, the DAO contracts were audited three times, and additionally reviewed by internal and external developers. These contracts are being used in production for more than a year, and have secured $500M worth of funds at the top of the market.
The Aragon Web client allows the user to vote on proposals about the DAO agent app making arbitrary calls, yet it doesn’t display what the call is about, which makes the whole feature rather useless. For DAOv1, I developed a CLI tool that allows the voters to verify that such a proposal does what the proposal creator says it does. So the proposal creator needs to announce what their proposal does off-chain — in this case through Github.
We used the CLI tool above with great success, yet this obviously wasn’t user-friendly enough for the stakers. As the solution, we decided to build both the arbitrary proposal creation and display features into the DAO dashboard. The only issue here was that the on-chain information was not enough to decode what the proposal is about. As a solution, we had the dashboard push the missing data as a part of the proposal metadata to the chain, which allowed all details of the proposals created by the dashboard to be displayable by the dashboard.
Aragon uses EVMScript to specify what a proposal does. Such a script can specify multiple function calls, somewhat similar to a Multicall contract. This essentially means that the API3 DAO supports the creation of a single proposal that sends Alice and Bob 20 USDC each, without using an additional contract. However, we decided to wall this feature off due to two reasons:
- It would have been additional UX design and front end implementation work, and we were short on time
- It could have been abused to make malicious function calls if the voters didn’t pay attention to all the function calls a proposal makes
Therefore, the DAO dashboard only allows the user to create proposals that make a single function call, and is only capable of displaying such proposals correctly.
With all this context, I was not content with two things:
- A final, clean audit report — which we couldn’t get due to audit scheduling difficulties — would have been nice to have for such a critical contract
- I was suspicious about the multicall functionality being walled off successfully at the front end (because even though I reviewed that work, I was stretched particularly thin at the time)
I contacted Hexens for a full DAO contract and front end audit, and I asked them specifically to look into the EVMScript encoding/decoding implementation in the front end. Their work was diligent, whose report you can see here, and they pointed out two highly critical issues, both are to do with EVMScript decoding:
- The attacker creates a proposal with an EVMScript that makes two function calls, the first one looking innocent and the second one being malicious. They set the metadata to make it seem like the proposal is created using the dashboard to only execute the first function call. To the voters, the proposal seems like it would only execute the first function call, causing the proposal to be passed and the malicious function call being executed.
- The attacker creates a proposal with an EVMScript that makes one malicious call, but the additional data they provide in the metadata makes it seem to be innocent (for example, metadata may state approve(address,uint256), while the function called is transfer(address,uint256)). Again, this causes the proposal to be passed and the malicious function call being executed.
Performing these attacks in an anonymous way is difficult, as it requires the attacker to be able to pass a proposal. On the other hand, passing such a secondary proposal would have allowed the secondary treasury to be drained, and passing such a primary proposal would have allowed the primary treasury and the staking pool to be drained.
The solution is quite simple, and is already implemented in the proposal verifier that I had built for DAOv1: After decoding the EVMScript, the front end should encode it again and compare with the original. If the two don’t match, this means the EVMScript doesn’t conform to the assumptions that the front end makes (in 1, that there will be a single function call, and in 2, the function signature in the metadata will be correct), and is likely malicious and should be voted against. We pushed this fix to production, and confirmed that none of the past proposals have exploited this.
See the audit report for the other issues and my responses. See this post about an announcement about one of the findings. Overall, I’d say that we got our clean audit report for the contracts (because the contract-related findings are intended behavior, but read the report and decide for yourself), and made sure that the front end is secure.