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
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, no heartbeat, no answeredInRound checks (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);
(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 exposing latestRoundData() 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.
Summary
Auditor.assetPrice()trustsIPriceFeed.latestAnswer()and only checksprice > 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():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:
assetPrice()(e.g. checkLiquidation, calculateSeize).Impact
Condition and likelihood: collateral oracle is stale at an old higher price during a drop (common during L2 sequencer incidents or oracle delays).
Flow:
Market.borrow(...).Auditor.checkBorrow()→accountLiquidity()uses the stale high price, inflating collateral value → borrow passes.After: once the oracle updates, the position becomes underwater; liquidation may not fully cover → bad debt.
Condition and likelihood: oracle is stale at an old lower price during a pump.
Flow
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
IPriceFeed.latestAnswer()with a Chainlink-style interface exposinglatestRoundData()and enforce freshness:@cruzdanilo @pmolina @lucaslain
Pls take a look at this, as i have other issues of bugs i would be submitting for this contract.