<!-- canonical: https://0xsimao.com/findings/evro-finance-ii-sdai-price-donation-manipulatable -->

# SDAIPriceFeed is vulnerable to donation attack via manipulatable vault rate

Crit/High · Sherlock · EUR stablecoin · 26th December 2025

Finding H-1 of the Evro Collateral Onboarding security review.

- Report: /reports/evro-collateral-onboarding
- Source: https://github.com/0xsimao/audits/blob/main/Sherlock/private-audits/2025-12-26-evro-finance-ii.pdf

---

## Summary
The `SDAIPriceFeed` uses the instantaneous vault rate from `convertToAssets()` which can be manipulated through donation attacks, allowing attackers to drain the protocol.

## Vulnerability Detail
The `SDAIPriceFeed` calculates the sDAI price using the ERC4626 vault's `convertToAssets()` function, which returns the current exchange rate between sDAI shares and DAI assets. This rate can be manipulated by donating assets directly to the vault.

Attack scenario:
1. Attacker deposits 500 sDAI when vault has 500 sDAI shares and 500 DAI (1:1 rate)
2. Attacker donates 9000 DAI directly to the vault
3. Now 1000 sDAI shares = 10000 DAI (1:10 rate)
4. Attacker opens a trove with 1000 sDAI and borrows 10000 EVRO at the inflated rate
5. Attacker redeems the 10000 EVRO against other collaterals, draining the protocol

## Impact
An attacker can manipulate the sDAI price to borrow significantly more EVRO than the collateral is worth, then redeem against other collaterals to drain the protocol.

## Code Snippet
https://github.com/sherlock-audit/2025-12-evro-finance-dec-26th/blob/main/evro/contracts/src/PriceFeeds/SDAIPriceFeed.sol#L47

```solidity
function _fetchPricePrimary() internal returns (uint256, bool) {
    assert(priceSource == PriceSource.primary);

    (uint256 daiUSDPrice, bool daiEurOracleDown) = _getOracleAnswer(ethUsdOracle);
    (uint256 eurUsdPrice, bool eurUsdOracleDown) = _getOracleAnswer(eurUsdOracle);

    // Get the DAI rate of the SDAI vault
    uint256 sdaiDaiRate = sdai.convertToAssets(1e18);  // @audit - manipulatable!

    // ...

    uint256 sdaiUSDPrice = FixedPointMathLib.mulWadUp(daiUSDPrice, sdaiDaiRate);
    lastGoodPrice = FixedPointMathLib.divWad(sdaiUSDPrice, eurUsdPrice);

    return (lastGoodPrice, false);
}
```

## Tool Used
Manual Review

## Recommendation
Use the max price between an oracle rate and the instantaneous rate for redemptions, and the min price for calculating ICR when opening/adjusting troves:

```solidity
function _fetchPricePrimary(bool _isRedemption) internal returns (uint256, bool) {
    uint256 sdaiDaiRate = sdai.convertToAssets(1e18);
    (uint256 oracleRate, bool oracleDown) = _getOracleAnswer(sdaiOracle);

    uint256 rateToUse;
    if (_isRedemption) {
        rateToUse = LiquityMath._max(sdaiDaiRate, oracleRate);
    } else {
        rateToUse = LiquityMath._min(sdaiDaiRate, oracleRate);
    }
    // ... rest of calculation
}
```

Disclosed by 0xSimao (https://0xsimao.com/).

---

Related findings:

- [Donation attack possible, although unlikely, could make an initial deposit](https://0xsimao.com/findings/aegis-donation-although-unlikely-deposit): Aegis Staked YUSD
- [OffchainExchange::swapAmm() does not validate that txn.priceX18 > 0, allowing donation attacks](https://0xsimao.com/findings/vertex-offchain-validate-price-donation): Vertex
- [Borrowing is not vulnerable to an inflation attack, it's unnecessary to borrow when initializing the vault](https://0xsimao.com/findings/fuji-finance-inflation-attack-borrow-initializing): Fuji Finance
- [Missing priceFeedDisabled event in RedStonePriceFeedBase](https://0xsimao.com/findings/felix-price-disabled-red-stone): Felix Price Feeds
