Previous posts (#3, #4, #5, #6) hypothesized that we can take a step towards making DeFi “SAFU” by breaking down Security Audits For Users (SAFU) and illustrated that with case studies of DeFi stablecoin, Zk-Rollup payment, risk coverage and options projects.
This post dives into another interesting DeFi project — Primitive, a DeFi options protocol.
To provide some context for new readers (others may skip this section), DeFi projects rely on external security audits as a stamp of security approval. Audit is not a security warranty of “bug-free” code by any stretch of imagination but a best-effort endeavour by trained security experts operating within reasonable constraints of time, understanding, expertise and of course, decidability.
Audit reports illustrate security issues with descriptions of underlying vulnerabilities, likelihood/impact, potential exploit scenarios and recommended/resolved fixes. They also provide subjective insights into code quality, documentation and testing. The scope/depth/format of audit reports varies across auditing teams but they generally cover similar aspects.
Security companies execute audits for clients who pay for their services. Engagements are therefore geared towards priorities of project owners and not project users. Audits are not intended to alert potential project users of any inherent risk. That is not their business/technical goal.
Nevertheless, the conjecture is that project users may be basing their risk without considering if the project had any security audits or incorrectly assuming no/minimal risk for audited projects without bothering to, or having the expertise (understandably so) to evaluate audit report contents.
Evaluating audit reports requires a reasonable level of expertise in smart contract security. Breaking down security audit reports for the benefit of less security-savvy users may make DeFi a bit safer by helping them DYOR.
This post gives a breakdown of Primitive protocol audit from OpenZeppelin where it draws attention to key takeaways in the context of the recent critical vulnerability disclosure and whitehat rescue.
Context
Project Overview: Primitive is an options market protocol.
Liquidity providers can earn yield on their DAI, ETH, or DeFi tokens through providing liquidity to the respective option markets. Yield is earned through trading fees generated on the supported AMMs (Sushiswap).
Traders can swap their DAI, ETH, or DeFi tokens to the respective Primitive Option tokens, giving them leveraged exposure in either direction. Call options can be purchase for the underlying tokens (ETH, DeFi tokens) to gain upside exposure, while Put options can be purchased for Dai to get downside protection. Short option tokens for both Calls and Puts can also be swapped to directly.
Option Writers can collateralize the options and sell them to earn upfront returns on their DAI, ETH, or DeFi tokens. When options expire, they release the remaining collateral held in the smart contract. In the case of an option expiring out-of-the-money, the initial collateral deposit would be released. In the case that an option is exercised, option writers are entitled to be paid the strike price of the option.
Primitive claims to solve a market need for leverage/hedging instruments and fee generating limited impermanent loss pools.
Vulnerability Disclosure & Response Overview: As outlined in Primitive’s post-mortem report, a critical vulnerability was discovered on 20 Feb, 2021 by the Dedaub team which was confirmed by the Immunefi team and then responsibly disclosed to Primitive Finance.
Vulnerability: The vulnerability was in uniswapV2Call of UniswapConnector03.sol, where it does not check the initiator of the flash-swap operation but only that the callback comes from Uniswap. And the steps to potential exploitation are best described in the post-mortem, as shown below:
Find victims, i.e., contracts that have “approved” the vulnerable contract for their tokens. Let’s call these “real” tokens.
Create a fake, worthless token.
Create a malicious, worthless option token that is based on the real token (“underlying”) and the above fake token (“redeem”).
Create a Uniswap liquidity pool, allowing trading the worthless “redeem” token for the real, “underlying” token. This pool can be financed via a flash loan, since it will be restored to the original balances at the end of the attack.
Start a flash swap that asks for the victim’s amount of real tokens and then invokes the entry-point function, flashMintShortOptionsThenSwap, with the right parameters. The vulnerable contract will start executing with real tokens in hand.
Pass the real tokens to the malicious option contract.
Ask it to mint malicious-option tokens. The malicious option then transfers the real tokens to the attacker.
Give the option tokens to the victim account.
Settle the flash swap balance by paying out of the victim’s funds.
The result is that the victim’s funds are transferred to the fake option contract, while leaving the Uniswap pool with the same amount of (real) tokens.
The pool can now be dismantled to recover its underlying tokens that were originally put in. (E.g., to repay the flash loan, in the implemented version of the attack.)
The net effect is that the attacker is left with the victim’s real tokens.
Response: Given that the vulnerable contracts were neither pausable nor upgradeable, the only options were to inform users to reset approved allowances and whitehat hack the remaining vulnerable funds to safety. Primitive Finance therefore engaged Emiliano from ReviewsDAO to set up a war room consisting of members from Primitive, Dedaub, ReviewsDAO, Immunefi and OpenZeppelin teams.
The war room assessed the funds at risk, possible options and prepared contracts to retrieve the vulnerable funds. Reaching out to known address holders to reset allowances reduced the exposed funds by 35%. The app’s frontend was updated to prevent more funds from becoming vulnerable. Three wallets were then whitehat attacked to rescue 98% of vulnerable funds. Finally, an announcement was made to remaining vulnerable users about steps to safeguard their funds as shown below:
Dedaub was awarded a bounty of $25,000 USDC for responsible bug disclosure and Emiliano $10,000 USDC for whitehack assistance.
Audit Overview: OpenZeppelin’s audit of Primitive reported on 21 August 2020 (duration/effort not mentioned) finding 3 critical (fixed/justified), 3 high (fixed/justified), 9 medium (unaddressed/partial/fixed/justified) and 11 low (unaddressed/partial/fixed/justified) severity issues along with 9 other information notes (unaddressed/partial/fixed). The status of fixes/justifications were at the time of the report and the current status is unclear.
OpenZeppelin Report Breakdown
OpenZeppelin’s audit report is well detailed starting with a list of contracts in audit scope, an overview of system semantics and privileged roles, before describing each finding in great detail to justify the severity.
Highlights: The key highlight from the audit was about the project being an early version, a work-in-progress, not ready for production without significant improvements to architecture, development process and further security reviews, as shown below:
The second highlight is that the audit scope covered Primitives.sol, Option.sol, Redeem.sol and Trader.sol (along with required interfaces and libraries). It specifically excluded Registry.sol, OptionsFactory.sol, RedeemFactory.sol and UniswapConnector03.sol (the vulnerable contract). This is reflected even in their documentation as shown below (see “Audited” column):
The final highlight is that in response to finding H03, which cautioned about the potential for a malicious owner to cause DoS via contract pause functionality, the project removed the pause functionality. This is interesting because pause is typically a recommended “guarded launch” scenario during initial stages of a deployed project, and the audit recommended only documenting the risk from this functionality and then to consider alternatives in later phases. But removing it affected the disclosure response as described above.
Findings: C01, C03 and H01 were related to direct interactions with the Option contract and were resolved with the understanding that this was not allowed (per design), and the recommended method was to interact via Trader or other wrapper contracts. C02 was related to different criteria being used to determine the expiry of options by close function and notExpired modifier.
H02 was about missing safety checks for flash loans but was acknowledged as not required in this flow. H03 was about the dangers of pause functionality to decentralized protocol governance as the protocol gains traction, where the recommendation was only to document this aspect but the project instead chose to remove this feature entirely.
Other findings were related to business logic and security/programming best practices to reduce attack surface and improve readability. These included aspects related to fees, expired options, flash loan implementation, contract/interface mismatches, unified pausing/unpausing, missing/incorrect events, different decimals of ERC20 tokens, missing contract-existence check, unnecessary getters, missing input validation, misleading names/comments, inconsistent coding style, unpinned dependencies and unused constructs.
The audit concluded with: “We audited an early version of the Primitive project that is a work-in-progress and not yet ready for production. This first audit round has been Primitive’s initial step on its way to reach the needed level of maturity for projects intended to handle large sums of financial assets. To further help the project reach a production-ready state, we highly advise additional rounds of security reviews.”
Conclusion
This post gave a breakdown of audit report on Primitive — a DeFi options trading protocol — from OpenZeppelin, in the context of the recent vulnerability disclosure and whitehat response operation by a collective effort from Dedaub, Immunefi, ReviewsDAO and Primitive.
Overall, there were some red flags raised by the audit, six months before the responsible disclosure, which were presumably fixed. But the reported vulnerability unfortunately was present in a contract not in scope for OpenZeppelin’s audit and without any further audits (as recommended by OpenZeppelin) before deployment, this remained undiscovered. Without the commendable collective effort of the security community, this could have been exploited sooner or later.
Security hindsight, after an exploit/disclosure, is certainly 20/20 and regretful. Better specification, more documentation, extensive testing of corner cases, use of security tools during development, internal code reviews, multiple independent audits covering the same/entire deployment scope for redundancy/completion and a public bug bounty program would help raise the bar.
The reported vulnerability was in a contract that was out of scope for the conducted audit. Also, it was not a common Solidity coding pitfall that could have been detected by automated tools checking for security best practices. It was a complex design/validation error and was particular to the exploited contract’s logic and its interaction with other protocols. A careful internal design/code review, or a second audit review closer to production deployment and covering previously unaudited contracts could probably have caught it. Finally, not removing the pause functionality could have made the whitehat response easier.
Audits provide a project’s security snapshot over a brief (typically few weeks) period. In this case, the audit was on an early version of the protocol which didn’t cover all the contracts developed/deployed post-audit. Smart contracts understandably need to evolve over time to add new features, fix bugs or optimize. Relying on external audits after every change is impractical. We observe from Primitive’s incident that unaudited deployed contracts, even if they are connector ones and not part of core functionality, introduce critical vulnerabilities.
Furthermore, vulnerabilities can be reduced but cannot be ruled out. Guarded launches with built-in circuit breakers (such as the pause functionality) and better on-chain monitoring/response are therefore critical.
Projects need 24/7 security expertise and should follow Secure Software Development Lifecycle (SSDLC) with “Continuous Auditing” and “Continuous Monitoring/Response” (similar to CI/CD process). Addressing this is both a hard market problem and a technical challenge. There isn’t enough supply of competent smart contract security engineers to keep up with the market demand. But there also isn’t a structured way to acquire smart contract security expertise today. The educational content is scattered, insufficient and somewhat outdated. Security experts are in such great demand to find/fix security problems that they don’t have the time/incentive to train newcomers. This is a scaling problem.
Breaking down DeFi Security Audits For Users (SAFU) may be worthwhile as a public good but certainly should not be interpreted as an endorsement/indictment of projects/auditors or as financial/investment advice. DeFi SAFU should be a collective community responsibility.
I hope you found this somewhat useful. Thanks for reading and look forward to your comments and feedback.