In Exactly, the maximum amount a user can borrow is calculated with the conversion rate between collateral and debt:
//Auditor.sol
function checkBorrow(Market market, address borrower) external {
......
// verify that current liquidity is not short
(uint256 collateral, uint256 debt) = accountLiquidity(borrower, Market(address(0)), 0);
if (collateral < debt) revert InsufficientAccountLiquidity();
}
And accountLiquidity is calculated with prices of each assets, which is fetched by calling the oracle's assetPrice function.
//Auditor.sol
function accountLiquidity(
address account,
Market marketToSimulate,
uint256 withdrawAmount
) public view returns (uint256 sumCollateral, uint256 sumDebtPlusEffects) {
AccountLiquidity memory vars; // holds all our calculation results
// for each asset the account is in
uint256 marketMap = accountMarkets[account];
for (uint256 i = 0; marketMap != 0; marketMap >>= 1) {
if (marketMap & 1 != 0) {
......
// get the normalized price of the asset (18 decimals)
vars.price = assetPrice(m.priceFeed);
// sum all the collateral prices
sumCollateral += vars.balance.mulDivDown(vars.price, baseUnit).mulWadDown(adjustFactor);
// sum all the debt
sumDebtPlusEffects += vars.borrowBalance.mulDivUp(vars.price, baseUnit).divWadUp(adjustFactor);
......
}
}
}
unchecked {
++i;
}
}
}
However, Chainlink price oracles are susceptible to front-running as their prices tend to lag behind an asset's real-time price. More specifically, Chainlink oracles are updated after the change in price crosses a deviation threshold, which means a price feed could return a value slightly smaller/larger than an asset's actual price under normal conditions.
An attacker could exploit the difference between the price reported by an oracle and the asset's actual price to gain a profit by front-running the oracle's price update.
The likelihood of this condition becoming true is significantly increased when PriceFeedDouble.sol is used as the market's oracle with multiple Chainlink price feeds. As seen from above, the conversion rate between collateral token/loan token and USD is calculated with multiple price feeds, with each of them having their own deviation threshold. This amplifies the maximum possible price deviation returned by assetPrice(m.priceFeed).
For example:
Now on the ethereum network, all Exactly oracles are denominated in ETH:
Assume a user has WBTC as collateral and DAI as loan.
Assume the following prices:
– 1 BTC = 20 ETH
– 1 WBTC = 1 BTC
– 1 ETH = 3000 DAI
PriceFeedDouble is set up as such:
– priceFeedOne - WBTC / BTC, 2% deviation threshold.
– priceFeedTwo - BTC / ETH, 2% deviation threshold.
PriceFeedPool is set up as such:
– DAI/ETH, 1% deviation threshold.
Assume that all price feeds are at their deviation threshold:
– WBTC / BTC returns 98% of 1, which is 0.98.
– BTC / ETH returns 98% of 20, which is 19.6.
– DAI / ETH returns 101% of 0.0003, which is 0.000303.
The actual conversion rate of WBTC to DAI is:
– 0.98 * 19.6 / 0.000303 = 63392.74
– 1 WBTC = 63392.74 DAI.
Compared to 1 WBTC = 60000 DAI, the maximum price deviation is 5.7%.
To demonstrate how a such a deviation in price could lead to arbitrage:
The price of WBTC drops while DAI increases in value.
All three Chainlink price feeds happen to be at their respective deviation thresholds as described above, which means the oracle's price is not updated in real time.
An attacker sees the price discrepancy and front-runs the oracle price update to do the following:
– Deposit 1 WBTC as collateral.
– Borrow all avaliable DAI.
• Afterwards, the oracle's conversion rate is updated.
– Attacker's position is now unhealthy as his collateral is worth less than his loaned amount.
• Attacker back-runs the oracle price update to liquidate himself.
Impact
All profit gained from arbitrage causes a loss of funds for lenders as the remaining bad debt is socialized by them.
Consider implementing a borrowing fee to mitigate against arbitrage opportunities.
Ideally, this fee would be larger than the oracle's maximum price deviation so that it is not possible to profit from arbitrage.
Further possible mitigations have also been explored by other protocols:
BoRonGod
medium
Deviation in oracle price could lead to arbitrage
Summary
Deviation in oracle price could lead to arbitrage
Vulnerability Detail
In Exactly, the maximum amount a user can borrow is calculated with the conversion rate between
collateral
anddebt
:And
accountLiquidity
is calculated with prices of each assets, which is fetched by calling the oracle'sassetPrice
function.However, Chainlink price oracles are susceptible to front-running as their prices tend to lag behind an asset's real-time price. More specifically, Chainlink oracles are updated after the change in price crosses a deviation threshold, which means a price feed could return a value slightly smaller/larger than an asset's actual price under normal conditions.
An attacker could exploit the difference between the price reported by an oracle and the asset's actual price to gain a profit by front-running the oracle's price update.
The likelihood of this condition becoming true is significantly increased when
PriceFeedDouble.sol
is used as the market's oracle with multiple Chainlink price feeds. As seen from above, the conversion rate between collateral token/loan token and USD is calculated with multiple price feeds, with each of them having their own deviation threshold. This amplifies the maximum possible price deviation returned byassetPrice(m.priceFeed)
.For example:
Now on the ethereum network, all Exactly oracles are denominated in ETH:
WBTC/ETH = WBTC/BTC + BTC/ETH
Assume a user has WBTC as collateral and DAI as loan.
Assume the following prices: – 1 BTC = 20 ETH – 1 WBTC = 1 BTC – 1 ETH = 3000 DAI
PriceFeedDouble
is set up as such: – priceFeedOne - WBTC / BTC, 2% deviation threshold. – priceFeedTwo - BTC / ETH, 2% deviation threshold.PriceFeedPool
is set up as such: – DAI/ETH, 1% deviation threshold.Assume that all price feeds are at their deviation threshold: – WBTC / BTC returns 98% of 1, which is 0.98. – BTC / ETH returns 98% of 20, which is 19.6. – DAI / ETH returns 101% of 0.0003, which is 0.000303.
The actual conversion rate of WBTC to DAI is: – 0.98 * 19.6 / 0.000303 = 63392.74 – 1 WBTC = 63392.74 DAI.
Compared to 1 WBTC = 60000 DAI, the maximum price deviation is 5.7%.
To demonstrate how a such a deviation in price could lead to arbitrage:
Impact
All profit gained from arbitrage causes a loss of funds for lenders as the remaining bad debt is socialized by them.
Code Snippet
https://github.com/sherlock-audit/2024-04-interest-rate-model/blob/main/protocol/contracts/PriceFeedDouble.sol#L27-L29
Tool used
Manual Review
Recommendation
Consider implementing a borrowing fee to mitigate against arbitrage opportunities. Ideally, this fee would be larger than the oracle's maximum price deviation so that it is not possible to profit from arbitrage.
Further possible mitigations have also been explored by other protocols:
Angle Protocol: Oracles and Front-Running
Liquity: The oracle conundrum
Duplicate of #156