Rounding error in WUSDA can result in loss of user funds, especially when manipulated by an attacker
Lines of code
Vulnerability details
Impact
Due to floor rounding in Solidity, it is possible for rounding errors to affect functions in the WUSDA contract.
Specifically, when WUSDA::_usdaToWUSDA is invoked internally, if _usdaAmount is sufficiently small compared with MAX_wUSDA_SUPPLY (10M) and the total USDA supply then it is possible for the _wusdaAmount return value to round down to 0.
solidityfunction _usdaToWUSDA(uint256 _usdaAmount, uint256 _totalUsdaSupply) private pure returns (uint256 _wusdaAmount) { _wusdaAmount = (_usdaAmount * MAX_wUSDA_SUPPLY) / _totalUsdaSupply; }
When calling the WUSDA::deposit/depositTo functions, which call the internal WUSDA::_deposit function with this rounded _wusdaAmount value, the implication is that depositors could receive little/no WUSDA for their USDA. Additionally, calls to the WUSDA::withdraw/withdrawTo functions could be process without actually burning any WUSDA. This is clearly not desirable as USDA can be subsequently redeemed for sUSD (at the expense of the protocol reserves and its other depositors).
solidityfunction deposit(uint256 _usdaAmount) external override returns (uint256 _wusdaAmount) { _wusdaAmount = _usdaToWUSDA(_usdaAmount, _usdaSupply()); _deposit(_msgSender(), _msgSender(), _usdaAmount, _wusdaAmount); } function withdraw(uint256 _usdaAmount) external override returns (uint256 _wusdaAmount) { _wusdaAmount = _usdaToWUSDA(_usdaAmount, _usdaSupply()); _withdraw(_msgSender(), _msgSender(), _usdaAmount, _wusdaAmount); }
Even when the USDA supply is sufficiently small to avoid this issue, an attacker could still utilize a sUSD flash loan to artificially inflate the USDA total supply which causes the down rounding, potentially front-running a victim such that their deposit is processed without minting any WUSDA and then withdrawing the inflated USDA supply to repay the flash loan.
Proof of Concept
normal case:
- Total USDA supply is 10B.
- Alice calls
WUSDA::depositwith_usdaAmountof 1e23 (10_000 tokens). _wusdaAmountis rounded down to 1e20/100 tokens (1e23 * 1e25 / 1e28 = 1e20).- Alice receives 100 WUSDA rather than 10_000.
- In reality, the total supply of sUSD is around 40M, so a more realistic scenario assuming 10M total USDA supply may be that Alice deposits 10e18 and receives 1 WUSDA or 1/10th the expected amount (10e18 * 1e25 / 1e26 = 1e18).
flash loan & attacker front-running case:
- Total USDA supply is 1M.
- Alice calls
WUSDA::depositwith_usdaAmountof 9.9e22 (99_000 tokens). - Attacker front-runs this transaction and calls
WUSDA::depositwith_usdaAmountof 3.3e22 (33_000 tokens). - Attacker receives 33_000 WUSDA.
- Attacker takes out a sUSD flash loan of 39M.
- Attacker deposits 39M sUSD into USDA.
- Alice's transaction is included here, but
_wusdaAmountis rounded down to 24_750 tokens (9.9e22 * 1e25 / 4e25 = 2.475e22). - Alice receives 24_750 WUSDA rather than 99_000 (only 1/4 of the expected amount).
- Attacker backruns Alice's transaction and calls
WUSDA::withdrawwith_usdaAmountof 132e21 (132_000 tokens). - Using the same rounding logic,
_wusdaAmountis rounded down to 33_000 tokens (132e21 * 1e25 / 4e25 = 33e22). - Attacker withdraws 132_000 WUSDA and receives the 99_000 USDA deposited by Alice in addition to their original 33_000.
- Attacker redeems USDA, repays the flash loan and profits 90_000 USDA/sUSD at the expense of Alice.
Tools Used
Manual review.
Recommended Mitigation Steps
Consider reverting if either WUSDA::_usdaToWUSDA or WUSDA::_wUSDAToUSDA return a zero value. Also consider multiplication by some scalar precision amount. Protections against manipulation of USDA total supply within a single block would also be desirable, perhaps achievable by implementing some multi-block delay.
Assessed type
Other
