-
Notifications
You must be signed in to change notification settings - Fork 38
Description
Summary
Auditor.assetPrice() trusts IPriceFeed.latestAnswer() and only checks price > 0. But it does not verify that the oracle value is fresh (no timestamp/round/heartbeat validation).
During a real world Chainlink stalls (sequencer downtime, feed pauses/delays, node issues), latestAnswer() commonly returns the last valid price without reverting, which this protocol will treat as current.
-
This can enable two issues in this contracts:
-
(1) over-borrow using stale-high collateral prices (bad debt / lender loss)
-
(2) liquidations using stale-low collateral prices (steal from users).
Root cause
Auditor.assetPrice():
function assetPrice(IPriceFeed priceFeed) public view returns (uint256) {
if (address(priceFeed) == BASE_FEED) return basePrice;
int256 price = priceFeed.latestAnswer();
if (price <= 0) revert InvalidPrice();
return uint256(price) * baseFactor;
}
- No
updatedAt, noheartbeat, noansweredInRoundchecks(the IPriceFeed interface only exposes latestAnswer()/decimals()).
If you notice this price is used throughout critical logic, e.g. collateral/debt valuation:
vars.price = assetPrice(m.priceFeed);
sumCollateral += vars.balance.mulDivDown(vars.price, baseUnit).mulWadDown(adjustFactor);
sumDebtPlusEffects += vars.borrowBalance.mulDivUp(vars.price, baseUnit).divWadUp(adjustFactor);
- and borrow eligibility:
(uint256 collateral, uint256 debt) = accountLiquidity(borrower, Market(address(0)), 0);
if (collateral < debt) revert InsufficientAccountLiquidity();
- Liquidation math also depends on
assetPrice()(e.g. checkLiquidation, calculateSeize).
Impact
- Over-borrow with stale-high collateral price → protocol/lenders eat bad debt
Condition and likelihood: collateral oracle is stale at an old higher price during a drop (common during L2 sequencer incidents or oracle delays).
Flow:
- Attacker deposits collateral and enters market.
- Calls
Market.borrow(...). Auditor.checkBorrow()→accountLiquidity()uses the stale high price, inflating collateral value → borrow passes.- Attacker exits with borrowed assets.
After: once the oracle updates, the position becomes underwater; liquidation may not fully cover → bad debt.
- Unfair liquidation with stale-low collateral price → steal from users
Condition and likelihood: oracle is stale at an old lower price during a pump.
Flow
- A borrower who is healthy at the real price appears unhealthy at the stale-low price.
- Liquidator calls Market.liquidate(...).
Auditor.checkLiquidation()/Auditor.calculateSeize()use the stale-low collateral price, enabling liquidation and determining seize amounts on wrong inputs.
Impact: borrower loses collateral even though they were healthy at real market prices; liquidator profits.
Recommendation
- Replace
IPriceFeed.latestAnswer()with a Chainlink-style interface exposinglatestRoundData()and enforce freshness:
require(updatedAt >= block.timestamp - MAX_STALENESS)
- Make staleness thresholds per-feed configurable (governance-set) since heartbeats differ across assets/chains.
@cruzdanilo @pmolina @lucaslain
Pls take a look at this, as i have other issues of bugs i would be submitting for this contract.