Full Report
The Yield Protocol is a fixed-rate borrowing and lending protocol on Ethereum. As demonstrated by the name "Yield", getting yield from the assets provided is an extremely important part of this protocol. With ERC20 tokens liquidity provider tokens, the mint() and burn() functions are common for adding liquidity and removing it from the protocol. mint() will create LP tokens from the provided asset token provided. burn() will destroy the LP token and give back the original asset token. These tokens are used for portions of the pool rewards. In the burn() function, all of the users tokens are burned. Then, based upon the amount of tokens burned and their share in the pool, it will give them the underlying token back. The code for this is below: uint256 burnt = _balanceOf[address(this)]; _burn(address(this), burnt); poolTokensObtained = pool.balanceOf(address(this)) * burnt / totalSupply_; The attacker can donate a large set of tokens to inflate the balance of the pool. This leads the pool to the pool sending more tokens to a user than they should. Crazily enough, these donated funds are not lost though! An attacker can call mint(), which will use the difference between the balance and the cached pool. So, the inflation of the amount of tokens being sent to the attacker doesn't cost them anything. The article has some interesting insights into the development process. First, cache-based contracts are known to be vulnerable to donation attacks if the developer is not careful. The author mentions going through the YieldSpace-TV project and validating that every single use of balanceOf() was not vulnerable. This new feature was audited and missed. However, they mention that the level of complexity of this bug warranted a code based audit. The time pressure led to an internal audit instead. In this case, the bug bounty program saved the day, which is amazing! Having multiple levels like this prevents major hacks from happening. Once the vulnerability was discovered, the protocol decided to use the eject() function to take all of the funds. They learned a few lessons from this warroom. First, having a pause() would have allowed them to explore their options without an attack being viable. Second, the contract is not upgradable but uses the code>eject() functionality to recover funds. By having the ability to upgrade contracts, restoring the protocol would have been much easier. Overall, an amazing post into the world of bug bounties, handling issues and protocol design. The fix is a single line change to use the cached version instead of the balance of the pool.
Analysis Summary
# Incident Report: Yield Protocol Strategy v2 Donation Vulnerability
## Executive Summary
On May 19, 2023, Yield Protocol was notified through Immunefi of a critical "donation attack" vulnerability in its Strategy v2 contracts. The bug allowed an attacker to manipulate pool balances to extract significant Liquidity Provider (LP) funds. No funds were lost due to the timely report; the protocol team successfully executed an emergency "eject" function to secure all assets before an exploit could occur.
## Incident Details
- **Discovery Date:** May 19, 2023
- **Incident Date:** Vulnerability introduced during Strategy v2 deployment (Pre-exploit discovery)
- **Affected Organization:** Yield Protocol
- **Sector:** Decentralized Finance (DeFi) / Ethereum Fixed-Rate Lending
- **Geography:** Global / Distributed
## Timeline of Events
### Initial Access
- **Date/Time:** May 19, 2023
- **Vector:** Bug Bounty Submission (Immunefi)
- **Details:** A security researcher identified a logic error in `Strategy.sol` at line 363 regarding how underlying tokens were calculated during the `burn()` process.
### Lateral Movement
- **N/A:** As this was a smart contract vulnerability discovery rather than a network breach, lateral movement was not applicable. However, the logic allowed for repeated cycles of `mint()` and `burn()` to drain pool reserves.
### Data Exfiltration/Impact
- **Potential Impact:** A "significant part" of LP funds were at risk of being extracted.
- **Actual Impact:** $0 lost (Bug discovered and mitigated before exploit).
### Detection & Response
- **Detection:** Whitehat report via bug bounty platform.
- **Response Actions:** A war room was convened immediately. The team utilized the `eject()` function to pull liquidity out of YieldSpace and into a fail-safe mode.
## Attack Methodology
- **Initial Access:** Identification of a coding flaw where `pool.balanceOf(address(this))` was used instead of a cached balance (`poolCached_`).
- **Persistence:** Not applicable (Smart contract exploit).
- **Discovery:** The attacker would "donate" a large set of tokens to inflate the balance of the pool.
- **Impact:**
1. Attacker donates tokens to the pool contract.
2. Attacker calls `burn()`, which calculates their share based on the inflated `balanceOf` rather than the intended cached value.
3. Attacker calls `mint()`, which uses the difference between the balance and the cached pool to "reclaim" the donated funds.
4. The process is repeated until the pool is drained.
## Impact Assessment
- **Financial:** No actual loss; however, the bounty resulted in a payout to the researcher.
- **Data Breach:** None.
- **Operational:** Protocol was forced into a "fail-safe" mode using the `eject()` function, requiring manual recovery.
- **Reputational:** Positive; the protocol proved its bug bounty program and emergency procedures worked effectively.
## Indicators of Compromise
- **Behavioral indicators:** Unexpectedly high `balanceOf` values relative to `poolCached_` counters; repeated cycles of `mint` and `burn` from a single address within a short timeframe.
## Response Actions
- **Containment:** Emergency execution of the `eject()` function.
- **Eradication:** Identification of the single-line fix (replacing `balanceOf` with `poolCached_`).
- **Recovery:** Funds moved to safe contracts; migration to fixed Strategy contracts.
## Lessons Learned
- **Coding Standards:** Cache-based contracts are notoriously vulnerable to donation attacks; the team had identified this risk previously but missed it in this specific iteration.
- **Audit Limitations:** The internal audit was insufficient for a contract holding the majority of the protocol assets. Time-to-market pressure led to skipping a contest-based audit.
- **Protocol Design:** The lack of a `pause()` mechanism limited response options, forcing the more "violent" `eject()` action.
- **Contract Upgradeability:** The non-upgradeable nature of the contracts made restoration more difficult than it would have been with an upgradeable proxy.
## Recommendations
- **Rigorous Auditing:** Adhere to "Smart Contract Risk Assessment" scores; if a score requires a contest-based audit (e.g., Code4rena or Spearbit), do not downgrade to internal reviews.
- **Standardized Heuristics:** Implement automated linting or static analysis to flag every instance of `balanceOf` in cache-based contracts.
- **Emergency Tooling:** Integrate `pause()` functionality into all asset-bearing contracts to provide a "circuit breaker" during incident response.
- **Bug Bounty Investment:** Maintain high critical bounty caps to ensure top-tier researchers prioritize the protocol.