Actionable DeFi Security Lessons from Compound’s Incidents
Decentralized Finance (DeFi) protocols have revolutionized financial services, offering permissionless lending, borrowing, and trading. And despite market fluctuations, DeFi protocols held over $100 billion in Total Value Locked (TVL) as of May 2025. This concentration of value makes robust security paramount.
Pioneering lending protocols like Compound (founded in 2017, with major versions V2 in 2019 and V3 in 2022) offer critical, battle-tested lessons from their operational history and security incidents. Our extensive work in DeFi security has consistently shown that a deep understanding of the root causes of exploits – from smart contract vulnerabilities to access control failures – is key to building resilient systems.
In this article, we’ll explore key security best practices for DeFi protocols, focusing on real-world bug fixes and patches from Compound and related projects. By analyzing past vulnerabilities and their resolutions, we can extract actionable strategies to enhance smart contract security, minimize risks, and build more resilient financial infrastructure.
The TrueUSD Case: Lessons for Secure DeFi Integrations
The TrueUSD (TUSD) token had an unusual implementation where its new version was deployed on a different address, but interactions with one address could trigger interactions with the other. At first glance, this behavior appeared consistent, but it could introduce a critical issue in the protocols relying on the token: if a protocol assumes token address verification is sufficient, this assumption breaks when a token has multiple entry points.
This was exactly the case with Compound’s sweepToken function, which was designed to withdraw non-underlying tokens. The function verified that the token being withdrawn was not the protocol’s underlying asset, but due to TUSD’s behavior, this check failed to provide the expected security guarantee.
function sweepToken(EIP20NonStandardInterface token) override external {
require(address(token) != underlying, "CErc20::sweepToken: can not sweep underlying token");
uint256 balance = token.balanceOf(address(this));
token.transfer(admin, balance);
}
The fix was implemented on the TUSD side by disabling the proxy mechanism in the legacy contract.
Key Takeaways from This Issue
📌Smart contract logic that extends standard token verification must be carefully designed and tested.
- DeFi protocols should avoid modifying expected token behavior when interacting with external assets.
- Tokens used in DeFi ecosystems should adhere strictly to established standards to prevent unintended interactions.
📌DeFi protocols must thoroughly verify contracts before integrating them.
In addition to performing thorough audits and due diligence, DeFi protocols are strongly encouraged to implement a whitelisting mechanism for third-party contracts. This ensures that only pre-approved and verified contracts can interact with the protocol.
- Although Compound itself did not contain a direct bug, this integration introduced a security risk to the protocol.
- Any external contract interaction represents a potential threat and should be carefully analyzed before deployment.
📌DeFi protocols should assume that external contracts may behave unpredictably.
- Instead of relying on external contracts, protocols should assume that the contract they interact with can have any behaviour, even malicious ones.
In the case of the sweepToken function, a more secure approach would involve:
- Checking token balances before and after the transfer to ensure expected behavior.
- Verifying that collateral balances remain unchanged, preventing unauthorized withdrawals of collateralized assets.
function sweepToken(EIP20NonStandardInterface token) override external {
require(address(token) != underlying, "CErc20::sweepToken: cannot sweep underlying token");
// Get initial balances
uint256 initialBalance = token.balanceOf(address(this));
uint256 initialCollateralBalance = getCollateralBalance(); // Function to fetch collateral balance
// Attempt transfer
token.transfer(admin, initialBalance);
// Check final balances
uint256 finalBalance = token.balanceOf(address(this));
uint256 finalCollateralBalance = getCollateralBalance();
require(finalBalance == 0, "CErc20::sweepToken: Transfer did not clear balance");
require(finalCollateralBalance == initialCollateralBalance, "CErc20::sweepToken: Collateral decreased unexpectedly");
}
Therefore, implementation integrations should include:
- Verification that the primary action has been executed correctly – in this case, ensuring that the token transfer was successfully completed and the expected balance reduction occurred.
- Ensuring that core requirements are not violated – such as making sure that collateral remains intact and has not been unintentionally withdrawn.
By incorporating these checks, the implementation becomes more robust against various non-standard token behaviors, such as fee-on-transfer tokens, rebase tokens, contracts with multiple entry points, or upgradable contracts that may introduce different logic in the future. This approach enhances security and reduces the risk of unintended interactions within DeFi protocols.
Empty Pool Attacks Lessons
One critical class of vulnerabilities found in Compound V2 and its forks stems from empty or near-empty lending pools. In such scenarios, the exchange rate between cTokens and the underlying asset becomes unstable due to Solidity’s integer arithmetic and rounding behavior. When totalSupply is very low or close to zero, attackers can artificially inflate the totalCash value of a market without increasing totalSupply, drastically boosting the exchange rate and thereby the collateral value of their cTokens. This manipulated collateral can then be used to borrow large amounts from other pools, causing significant losses.
exchangeRate = (totalCash + totalBorrows - totalReserves) / totalSupply
This means that if the numerator becomes large (due to inflated totalCash) while the denominator (totalSupply) remains very small, the resulting exchange rate becomes abnormally high. As a result, the value of each cToken is artificially inflated, allowing attackers to use a small amount of capital to borrow disproportionately large amounts from other markets.
When totalSupply is near zero, even a tiny deposit directly to the cToken contract can artificially inflate the exchange rate, allowing an attacker to borrow much more than they should.
This is a known issue in the Compound codebase, and as such, any newly deployed pools must be launched under strict supervision to ensure they are initialized with sufficient liquidity and properly configured to prevent potential exploitation.
Key Takeaways from This Issue
📌Initialize Pools with Sufficient Liquidity at Deployment
- DeFi protocols should require an initial liquidity provision during contract deployment or initialization. This prevents attackers from being the first to interact with an empty pool and minting a disproportionate share of pool tokens at manipulated exchange rates.
constructor(address underlying, uint256 initialAmount) {
require(initialAmount >= 1e6, "Must provide initial liquidity");
_mint(msg.sender, initialAmount);
underlyingToken.transferFrom(msg.sender, address(this), initialAmount);
}
📌Burn Residual cTokens in Inactive Pools
- Monitor inactive or low-activity markets. If total supply or reserves fall below a safe threshold, consider burning leftover pool tokens or freezing the market to avoid rounding errors or precision loss that attackers can exploit.
function cleanupDormantPool() external onlyAdmin {
if (totalSupply() < 1e6) {
_burn(address(this), balanceOf(address(this)));
}
}
📌Avoid Allowing Redemptions in Empty Pools
- Consider disabling the redeem functions or introducing rate-limiting/redemption guards when a market has low liquidity. Although this limits user flexibility, it can block attackers from finalizing the exploit path.
modifier hasSufficientLiquidity(uint256 amount) {
require(getAvailableLiquidity() >= amount, "Insufficient liquidity");
_;
}
function redeem(uint256 cTokens) external hasSufficientLiquidity(cTokensToUnderlying(cTokens)) {
// Redeem logic
}
Weak Oracle Integration Lessons
Several critical incidents in DeFi have stemmed from weak oracle integrations, where protocols relied on price feeds that could be manipulated or that failed to update accurately under volatile market conditions. These integrations introduced systemic risks that could be exploited for unfair liquidations, under-collateralized loans, or profit extraction.
DAI Liquidation Event 2020
The protocol used a price feed where any address could post signed prices from Coinbase Pro, anchored to ±20% of Uniswap TWAP. A short-term price spike on Coinbase manipulated the oracle input, triggering protocol-level liquidations.
Key Takeaways from This Issue
📌 Oracle inputs must be aggregated from multiple independent sources: Using a single data source makes the system fragile and easy to manipulate.
function getAggregatedPrice() public view returns (uint256) {
uint256 price1 = chainlinkOracle.latestAnswer();
uint256 price2 = bandOracle.getReferenceData("DAI", "USD").rate;
return _calculateAggregatedPrice(price1, price2);
}
📌 Relying on a centralized exchange as a primary price source may introduce a single point of failure: centralized sources can be illiquid, manipulated, or delayed – especially during volatility.
📌 Stop-loss mechanisms should be used to automatically pause the contract on abnormal price deviations.
if (abs(currentPrice - referencePrice) > threshold) {
paused = true;
}
Chainlink + Uniswap TWAP Inconsistency
The protocol rejected Chainlink price updates if they fell outside Uniswap TWAP bounds. Due to high TWAP latency, valid real-time prices were discarded. This led to outdated price usage and incorrect collateral valuations.
Key Takeaways from This Issue
📌Oracle systems must tolerate temporary desynchronization across sources.
- Different oracle feeds may have varying update intervals and latencies. Strict synchronization may cause valid data to be discarded.
function isPriceAcceptable(uint256 priceA, uint256 priceB) internal pure returns (bool) {
uint256 diff = priceA > priceB ? priceA - priceB : priceB - priceA;
return diff * 1e4 / priceB <= 500; // Allow up to 5% deviation
}
📌 Bounding mechanisms (e.g., TWAP-based anchors) can be dangerous during volatility if they override real-time data.
- TWAPs are slow-moving by design, they can lag significantly behind actual market prices.
function updatePrice(uint256 chainlinkPrice, uint256 twapPrice) external {
// Use Chainlink price if deviation is within tolerance
if (isPriceAcceptable(chainlinkPrice, twapPrice)) {
currentPrice = chainlinkPrice;
} else {
// Log deviation event, fallback to chainlink or skip update
emit PriceDeviationDetected(chainlinkPrice, twapPrice);
}
}
📌Fallbacks and grace windows are essential to prevent false positives in price validation.
- Without fallback mechanisms, valid price feeds may be discarded, degrading protocol functionality.
function safeUpdatePrice(uint256 newPrice) external {
if (block.timestamp < lastUpdateTime + 60) {
// Allow grace window before enforcing bounds
currentPrice = newPrice;
} else {
require(isValid(newPrice), "Invalid price outside window");
currentPrice = newPrice;
}
}
Re-entrancy vulnerabilities lessons
One Compound fork was exploited via reentrancy enabled by the ERC777 tokensReceived hook. The attacker supplied ETH as collateral, borrowed AMP (an ERC777 token), and during AMP’s transfer(), the tokensReceived hook triggered a reentrant call to borrow. Since the protocol transferred tokens before updating storage, it violated the Checks-Effects-Interactions (CEI) pattern, enabling repeated borrowings before state changes were finalized.
doTransferOut(borrower, borrowAmount, isNative);
/* We write the previously calculated values into storage */
accountBorrows[borrower].principal = vars.accountBorrowsNew;
accountBorrows[borrower].interestIndex = borrowIndex;
totalBorrows = vars.totalBorrowsNew;
/* We emit a Borrow event */
emit Borrow(borrower, borrowAmount, vars.accountBorrowsNew, vars.totalBorrowsNew);
The vulnerability existed specifically because of the interaction between ERC777 token mechanics and custom borrow logic in the forked protocol.
The protocol delisted all ERC-777 tokens and introduced a global reentrancy guard to mitigate similar attack vectors across the protocol.
Key Takeaways from This Issue
📌 Follow Checks-Effects-Interactions (CEI) pattern strictly: failing to update state before external calls enables reentrancy.
function borrow(uint256 amount) external nonReentrant {
_updateAccountState(msg.sender); // ✅ effects first
require(_isEligible(msg.sender, amount), "Not eligible");
_transfer(msg.sender, amount); // interaction last
}
📌 Use reentrancy guards in any function interacting with external contracts or tokens.
- Even trusted tokens may trigger unknown logic (e.g., hooks, callbacks, fallback functions).
Conclusion
The real-world incidents explored in this article – from unsafe token integrations and empty-pool exploits to weak oracle setups and reentrancy vulnerabilities – highlight recurring patterns that can be mitigated by disciplined engineering practices.
The key to building resilient DeFi infrastructure lies in:
- Strict adherence to smart contract design patterns like Checks-Effects-Interactions.
- Careful handling of external contract interactions, especially with non-standard tokens and complex token standards like ERC777.
- Robust oracle systems that aggregate data, tolerate latency, and incorporate fallbacks.
- Early-stage protections such as initial liquidity provisioning and safeguards for dormant markets.
By learning from the lessons of Compound and its forks, developers can proactively eliminate common pitfalls and designDeFi systems that are not only innovative but also fundamentally secure.
Subscribe
to our
newsletter
Be the first to receive our latest company updates, Web3 security insights, and exclusive content curated for the blockchain enthusiasts.

Table of contents
Tell us about your project
Read next:
More related- Uniswap V2 Core Contracts: Technical Details & Risks
11 min read
Discover
- Enterprise Blockchain Security: Strategic Guide for CISOs and CTOs
5 min read
Discover
- How Uniswap V4’s Truncated Oracle Addresses TWAP Vulnerabilities
6 min read
Discover