Summary
Get the collateralPrice or euroPrice should check the price are not the stale prices, otherwise will destroy the protocols' feature.
Vulnerability Details
Below functions don't check the prices are the latest updated prices.
* @notice Gets the user's collateral for this vault in euros
* @param _user The user for which the collateral is calculated
*/
function getUserVaultMeowllateralInEuros(address _user) external view returns (uint256) {
(, int256 collateralToUsdPrice, , , ) = i_priceFeed.latestRoundData();
(, int256 euroPriceFeedAns, , ,) = i_euroPriceFeed.latestRoundData();
uint256 collateralAns = getUserMeowllateral(_user).mulDiv(uint256(collateralToUsdPrice) * EXTRA_DECIMALS, PRECISION);
return collateralAns.mulDiv(uint256(euroPriceFeedAns) * EXTRA_DECIMALS, PRECISION);
}
* @notice Gets the total sum of collateral deposited in Aave and the collateral earned by interest from Aave
*/
function getTotalMeowllateralInAave() public view returns (uint256) {
(uint256 totalCollateralBase, , , , , ) = i_aavePool.getUserAccountData(address(this));
(, int256 collateralToUsdPrice, , , ) = i_priceFeed.latestRoundData();
return totalCollateralBase.mulDiv(PRECISION, uint256(collateralToUsdPrice) * EXTRA_DECIMALS);
}
Impact
KittyFi guarantee the COLLATERAL_PERCENT by below funciton, which based on the collateral and Euro's price. but if the prices are not the latest prices, That can leads to the user's COLLATERAL_PERCENT based on the wrong price either higer or lower than the required COLLATERAL_PERCENT. The formar situation:user can mint more kittyCoins, the latter situation: user forced to liquidated.
* @notice Checks if the user has enough Meowllateral
* @param _user address of the user
* @return hasEnoughCollateral true if user has enough Meowllateral
*/
function _hasEnoughMeowllateral(address _user) internal view returns (bool hasEnoughCollateral) {
uint256 totalCollateralInEuros = getUserMeowllateralInEuros(_user);
uint256 collateralRequiredInEuros = kittyCoinMeownted[_user].mulDiv(COLLATERAL_PERCENT, COLLATERAL_PRECISION);
return totalCollateralInEuros >= collateralRequiredInEuros;
}
Tools Used
Manual
Recommendations
According to chainlink pricefeed requirement. Set the corrospending's HEARTBEAT, to make sure the prices are the latest prices.
https://docs.chain.link/data-feeds/price-feeds/addresses/?network=ethereum&page=1
For places applying oracle price services, all add the validaitons.
Here, I just take ETH as example, when apply more other different collaterals, can do the related adjustments.
error KittyVault__ETHStalePriceFeed();
error KittyVault__EURStalePriceFeed();
uint256 private constant EUROPRICEFEED_HEARTBEAT = 86400;
uint256 private constant ETHPRICEFEED_HEARTBEAT = 3600;
function getUserVaultMeowllateralInEuros(
address _user
) external view returns (uint256) {
(
,
int256 collateralToUsdPrice,
,
uint256 collateralUpdatedAt,
) = i_priceFeed.latestRoundData();
require(
collateralUpdatedAt >= block.timestamp - ETHPRICEFEED_HEARTBEAT,
KittyVault__ETHStalePriceFeed()
);
(, int256 euroPriceFeedAns, , uint256 euroUpdatedAt, ) = i_euroPriceFeed
.latestRoundData();
require(
euroUpdatedAt >= block.timestamp - EUROPRICEFEED_HEARTBEAT,
KittyVault__EURStalePriceFeed()
);
uint256 collateralAns = getUserMeowllateral(_user).mulDiv(
uint256(collateralToUsdPrice) * EXTRA_DECIMALS,
PRECISION
);
return
collateralAns.mulDiv(
uint256(euroPriceFeedAns) * EXTRA_DECIMALS,
PRECISION
);
}
function getTotalMeowllateralInAave() public view returns (uint256) {
(uint256 totalCollateralBase, , , , , ) = i_aavePool.getUserAccountData(
address(this)
);
(, int256 collateralToUsdPrice, , uint256 updatedAt, ) = i_priceFeed
.latestRoundData();
require(
updatedAt >= block.timestamp - ETHPRICEFEED_HEARTBEAT,
KittyVault__ETHStalePriceFeed()
);
return
totalCollateralBase.mulDiv(
PRECISION,
uint256(collateralToUsdPrice) * EXTRA_DECIMALS
);
}