Full Report
DFX Finance is a decentralized foreign exchange protocol that allows users to swap many stablecoins. DFX is an AMM that exchanges tokens according to a bonding curve, which is dynamically calculated from Chainlink prices. Each one of the currencies is paired with USDC in a pool. There are two main parts to the DFX protocol: Assimilators and Curve. Assimilators allow the AMM to handle pairs of different values for fixed point arithmetic, since Solidity doesn't support floating point numbers. In particular, it's responsible for converting all amounts to numeraire, a base value for all computations. DFX Finance maintains the assimilators which integrate with Curve to provide proportional liquidity to pools. When users would provide liquidity to a pool to receive yield on their stablecoins, they call the deposit() for a Curve pool and receive LP tokens. During this process, a check occurs to see if the deposit amount is greater than zero. The EURS token is a token with only two decimals. When creating these sorts of protocols, the developers may not have considered these small decimal tokens. The amount to transfer from the user is calculated based upon the following values: amount_ = (_amount.mulu(10**tokenDecimals) * 1e6) / _rate; token.safeTransferFrom(msg.sender, address(this), amount_); This code leads to zero tokens being transferred from the user. Despite this transferring zero tokens, the attacker gets some value being put into the contract. However, it is an extremely small amount of LP tokens that are sent back to the user. Normally, tokens have at least six decimals. So, the amount of tokens gained from this would be tiny with these larger tokens. Because of how small the values normally are, the amount spent on gas would dwarf this. However, since the EURS token only has two decimals, this attack becomes viable! By depositing a tiny amount 10K times, an attacker can get $190 per attack by withdrawing the CURVE LP tokens. To fix this vulnerability, there is a simple check to make sure the amount of funds transferred is not zero. The hacker got a 100K bounty for a 230K pool; this is a good look for the protocol. Overall, this is a fascinating vulnerability that was only possible because of a single token. Precision issues are super interesting!
Analysis Summary
# Vulnerability: DFX Finance EURS Assimilator Precision Rounding Error
## CVE Details
- **CVE ID:** N/A (Web3/Smart Contract vulnerability disclosed via Bug Bounty)
- **CVSS Score:** 9.1 (Critical estimate based on total loss of pool funds)
- **CWE:** CWE-682: Incorrect Calculation (Rounding Error)
## Affected Systems
- **Products:** DFX Finance Protocol
- **Versions:** Protocol V2
- **Configurations:** Specifically the EURS/USDC liquidity pool and its associated `AssimilatorV2` contract.
## Vulnerability Description
The flaw exists in how the `AssimilatorV2` contract handles tokens with low decimal counts, specifically the EURS token which uses only 2 decimals.
In the `intakeNumeraireLPRatio` function, the protocol calculates the amount of tokens to transfer from a user during a deposit using the following logic:
`amount_ = (_amount.mulu(10**tokenDecimals) * 1e6) / _rate;`
Due to the low decimal count (2) of EURS, a precision loss (rounding down to zero) occurs. When an attacker provides a very small `_amount` value, the calculation results in `0`. The contract then calls `token.safeTransferFrom(msg.sender, address(this), 0)`, which succeeds without taking any funds from the user. However, the `Curve` pool logic still mints a non-zero (though tiny) amount of LP tokens to the attacker based on the intended `_amount`.
## Exploitation
- **Status:** PoC disclosed via Immunefi; Vulnerability mitigated before malicious exploitation.
- **Complexity:** Low (Requires high-frequency interaction)
- **Attack Vector:** Network (Smart Contract Interaction)
The attack is a "death by a thousand cuts" exploit. An attacker can repeatedly call the `deposit()` function with a value that rounds the transfer amount to zero but still yields a fraction of an LP token. By performing this ~10,000 times, the attacker accumulates enough LP tokens to withdraw approximately $190 per cycle, eventually draining the pool (which held ~$237,143 at the time of discovery).
## Impact
- **Confidentiality:** None
- **Integrity:** High (Manipulation of protocol accounting and LP token minting)
- **Availability:** High (Potential for complete drain of pool liquidity)
## Remediation
### Patches
- The protocol implemented a fix to the `AssimilatorV2` logic to ensure that any calculated transfer amount must be greater than zero.
- Protocol V2 updates on Polygon and Mainnet have addressed the rounding check.
### Workarounds
- Implement a `require(amount_ > 0, "DFX: ZERO_TRANSFER")` check in the transfer logic of the assimilator to prevent minting LP tokens for "free" deposits.
## Detection
- **Indicators of Compromise:** High frequency of small deposits from a single address that result in `Transfer` events of 0 tokens followed by `Mint` events of LP tokens.
- **Detection methods and tools:** Monitoring blockchain events for `safeTransferFrom` calls where the value is 0 while accompanying state changes (LP minting) are positive.
## References
- **Immunefi Bugfix Review:** hxxps[://]medium[.]com/immunefi/dfx-finance-rounding-error-bugfix-review-17ba5ffb4114
- **DFX Protocol V2 GitHub:** hxxps[://]github[.]com/dfx-finance/protocol-v2
- **EURS Assimilator Contract:** hxxps[://]polygonscan[.]com/address/0x2385d7ab31f5a470b1723675846cb074988531da#code