Full Report
Rounding bugs that lead to massive loss of funds have alluded me for a while. I see them in large hacks but don't understand where they're useful and how to find them. This post is a good step for me on that journey through the lends of accounting in the share and token model. In Solidity, there is only fixed precision. For instance, you can either have 1 or 2 but nothing in between. Some things, like ERC20, use the first 18 digits as being this decimal point though. Most of the time, 1 or 2 may not be a big different. However, given the right circumstances, it can make a huge difference. Many systems, such as ERC 4626 tokenized vaults, use the token to share model. When passing in tokens, the user gets back a share or percentage model of the system. These shares directly correspond to the value provided but may help with rewards as well. Over time, these shares can accrue more and more value. The authors give an example. Say we have shares to tokens with a one to one ratio to beginning this and start with 1000 shares. After accuring fees, the new ratio is 1001 tokens to 1000 shares. If a user takes out 999 of these shares, we run into a precision problem. Should they get 1000 tokens or 999 tokens? This demonstrates the first of the issues: rounding direction. Generally speaking, we should round against the user. So, we'll round to 999 tokens in this case. The direction of rounding is an important thing to consider, as small discrepancies can lead to lost value over time if exploited millions of times. Now, back to the weird situation from above with only 2 token being left in the vault. The ratio is now 2:1 for tokens to share! If a token is donated, this becomes even higher at 3:1. There are many cases of this inflation leading to weird situations that led to stolen funds. For Radiant Capital, who was hacked for 4.5M, it's a simple thing like we described above. If the inflation was done to make each share worth 1000 each, then the user withdraw $1999 dollars, only a single share could be burned. This gives them a free $999 because they still have the one share left. In Wise lending, there was a rounding issue in liquidation code - you shouldn't be able to bankrupt yourself. This is best explained by example. We have one share worth $1000 and try to borrow $500. Now, we try to withdraw $1 of our collateral. The code will round up to use our one share worth $1000 dollars, making it liquidatable. This is rounding against the user but stills causes problems. If they round down, they also have a bug, since we could steal money this way. What do? Force shares to be withdrawn and not the token itself. In the case of DeFi, these are a few important things to consider when exploiting these rounding attacks. First, the share value needs to be inflatable via the methods described above. Next, we need a nearly empty pool. Finally, we need the weird rounding or accounting to occur. To prevent this, the easiest way is to add assets to the vault at deployment or have an artificial shares at the beginning. If we zoom out from the vault case, there are some bigger takeaways. First, having large and small values interaction can cause major issues with rounding. Second, rounding directions are important to consider. By default, things will round down. So, evaluating each division and the consequence of the round seems like a good takeaway. Finally, edge cases are where the exploitability of these things occur. Good post!
Analysis Summary
# Vulnerability: Share Inflation and Rounding Precision Errors in DeFi Lending
## CVE Details
- **CVE ID**: N/A (Project-specific exploits: Radiant Capital & Wise Lending)
- **CVSS Score**: 7.5 (High - Estimated based on financial loss and exploitability)
- **CWE**: CWE-681: Incorrect Conversion between Numeric Types; CWE-190: Integer Overflow or Wraparound (Rounding-related)
## Affected Systems
- **Products**: DeFi Lending Protocols, ERC-4626 Tokenized Vaults.
- **Versions**: Various; specifically protocols using the "Share and Token" accounting model.
- **Configurations**:
- Pools with low liquidity or nearly empty states.
- Systems where the conversion rate (token:share) can be influenced by direct token transfers (donations).
- Protocols that allow the withdrawal of "tokens" rather than "shares," leading to internal rounding discrepancies.
## Vulnerability Description
The flaw resides in how smart contracts handle fixed-precision arithmetic (integers) during the conversion between underlying assets (tokens) and protocol representation (shares).
In Solidity, division always rounds down by default. Attackers can artificially inflate the value of a single share by donating tokens to an almost empty pool. If a protocol rounds down against the user during a withdrawal, but the shares are worth a massive amount (e.g., $1,000 per share), a user can withdraw a value just under the cost of two shares (e.g., $1,999) while the contract only burns one share, effectively leaving the remaining value to the attacker. Alternatively, rounding "up" can cause unintended liquidations by burning more collateral than intended.
## Exploitation
- **Status**: Exploited in the wild (Radiant Capital: ~$4.5M loss; Wise Lending: ~$460k loss).
- **Complexity**: Medium
- **Attack Vector**: Network (Smart Contract Interaction)
## Impact
- **Confidentiality**: None
- **Integrity**: High (Manipulation of financial balances/accounting)
- **Availability**: High (Drainage of protocol liquidity; "Self-bankrupting" via liquidation)
## Remediation
### Patches
- **Radiant Capital**: Updated rounding logic and precision handling post-exploit.
- **Wise Lending**: Modified withdrawal logic to prevent unexpected liquidations.
### Workarounds
- **Initial Liquidity Minting**: Permanently lock a "minimum liquidity" (e.g., 1000 shares) by minting them to a zero address at deployment to prevent the pool from being easily empty/inflatable.
- **Virtual Shares**: Implement "artificial shares" at the beginning of the vault life to offset inflation attacks.
- **Hardcoded Conversion Rates**: Manage conversion rates separately via interest indices rather than relying purely on the current balance of the contract.
## Detection
- **Indicators of Compromise**:
- Large "donations" of tokens to a vault followed by immediate small withdrawals or borrows.
- Vault states where the ratio of tokens to shares is extremely high (e.g., 1 share = 1,000,000+ tokens).
- **Detection Methods**:
- Use static analysis tools to flag `div` operations that lack explicit rounding direction handling.
- Implement Invariant Testing (e.g., Foundry/Echidna) to check if `tokenBalance` correctly matches `shares * price` after every state change.
## References
- **Radiant Capital Post-Mortem**: hxxps://medium[.]com/@_kcyw/radiant-capital-hack-explained-1633289be150
- **Wise Lending Analysis**: hxxps://etherscan[.]io/address/0x829c3AE2e82760eCEaD0F384918a650F8a31Ba18
- **Original Deep Dive**: hxxps://osec[.]io/blog/2024-01-18-rounding-bugs/