First Flight #21: KittyFi

First Flight #21
Beginner FriendlyDeFiFoundry
100 EXP
View results
Submission Details
Severity: high
Invalid

KittyPool::purrgeBadPawsition does not burn user's kittyCoins, users can farm kittyCoins

Summary

The KittyPool::purrgeBadPawsition function is likely intended to be called by liquidators (aka other users), not by the users themselves. However, when a liquidator calls this function, it does not burn the user's KittyCoins. Instead, it burns them from the msg.sender, which is the liquidator.

Vulnerability Details

The line

i_kittyCoin.burn(msg.sender, totalDebt);

in
KittyPool::purrgeBadPawsition function below:

function purrgeBadPawsition(address _user) external returns (uint256 _totalAmountReceived) {
require(!(_hasEnoughMeowllateral(_user)), KittyPool__UserIsPurrfect());
uint256 totalDebt = kittyCoinMeownted[_user];
kittyCoinMeownted[_user] = 0;
@> i_kittyCoin.burn(msg.sender, totalDebt);
uint256 userMeowllateralInEuros = getUserMeowllateralInEuros(_user);
uint256 redeemPercent;
if (totalDebt >= userMeowllateralInEuros) {
redeemPercent = PRECISION;
}
else {
redeemPercent = totalDebt.mulDiv(PRECISION, userMeowllateralInEuros);
}
uint256 vaults_length = vaults.length;
for (uint256 i; i < vaults_length; ) {
IKittyVault _vault = IKittyVault(vaults[i]);
uint256 vaultCollateral = _vault.getUserVaultMeowllateralInEuros(_user);
uint256 toDistribute = vaultCollateral.mulDiv(redeemPercent, PRECISION);
uint256 extraCollateral = vaultCollateral - toDistribute;
uint256 extraReward = toDistribute.mulDiv(REWARD_PERCENT, PRECISION);
extraReward = Math.min(extraReward, extraCollateral);
_totalAmountReceived += (toDistribute + extraReward);
_vault.executeWhiskdrawal(msg.sender, toDistribute + extraReward);
unchecked {
++i;
}
}
}

Impact

Users will be able to farm KittyCoins. When their positions are liquidated, they will receive their collateral back. However, they will still retain their KittyCoins, as they will not be burned.

Tools Used

PoC, placed inside KittyFiTest.t.sol:

function test__purrgeBadPawsitionBurningMsgSender() public {
address liquidator = makeAddr("liquidator");
deal(weth, liquidator, AMOUNT);
// Simulate the user minting KittyCoin
vm.startPrank(user);
IERC20(weth).approve(address(wethVault), AMOUNT);
kittyPool.depawsitMeowllateral(weth, AMOUNT);
kittyPool.meowintKittyCoin(AMOUNT * 1000);
vm.stopPrank();
// Simulate the liquidator minting KittyCoin
vm.startPrank(liquidator);
IERC20(weth).approve(address(wethVault), AMOUNT);
kittyPool.depawsitMeowllateral(weth, AMOUNT);
kittyPool.meowintKittyCoin(AMOUNT * 1000);
vm.stopPrank();
// Update the price feed to be 20 times the current price
ethUsdPriceFeedMock.updateAnswer(3000e8 / 2); // price of eth falls by 50%
// Check initial balances
uint256 liquidatorBalanceBefore = kittyCoin.balanceOf(liquidator);
console.log("liquidator balance before: ", liquidatorBalanceBefore);
uint256 userBalanceBefore = kittyCoin.balanceOf(user);
console.log("User balance before: ", userBalanceBefore);
// Ensure that the user no longer has enough collateral
require(!kittyPool._hasEnoughMeowllateral(user), "User still has enough collateral");
// Simulate liquidator trying to liquidate the user's position
vm.prank(liquidator);
uint256 totalAmountReceived = kittyPool.purrgeBadPawsition(user);
console.log("Total Amount Received by Liquidator: ", totalAmountReceived);
// Check balances after liquidation
uint256 liquidatorBalanceAfter = kittyCoin.balanceOf(liquidator);
console.log("liquidator balance after: ", liquidatorBalanceAfter);
uint256 userBalanceAfter = kittyCoin.balanceOf(user);
console.log("User balance after", userBalanceAfter);
assertEq(userBalanceAfter, userBalanceBefore, "User's balance should not change");
}

The code above demonstrates that the user's KittyCoin balance will not change when liquidation occurs, while the KittyCoin balance will decrease on the liquidator's side.

Recommendations

Replace msg.sender with parameter _user.

function purrgeBadPawsition(address _user) external returns (uint256 _totalAmountReceived) {
require(!(_hasEnoughMeowllateral(_user)), KittyPool__UserIsPurrfect());
uint256 totalDebt = kittyCoinMeownted[_user];
kittyCoinMeownted[_user] = 0;
- i_kittyCoin.burn(msg.sender, totalDebt);
+ i_kittyCoin.burn(_user, totalDebt);
uint256 userMeowllateralInEuros = getUserMeowllateralInEuros(_user);
uint256 redeemPercent;
if (totalDebt >= userMeowllateralInEuros) {
redeemPercent = PRECISION;
}
else {
redeemPercent = totalDebt.mulDiv(PRECISION, userMeowllateralInEuros);
}
uint256 vaults_length = vaults.length;
for (uint256 i; i < vaults_length; ) {
IKittyVault _vault = IKittyVault(vaults[i]);
uint256 vaultCollateral = _vault.getUserVaultMeowllateralInEuros(_user);
uint256 toDistribute = vaultCollateral.mulDiv(redeemPercent, PRECISION);
uint256 extraCollateral = vaultCollateral - toDistribute;
uint256 extraReward = toDistribute.mulDiv(REWARD_PERCENT, PRECISION);
extraReward = Math.min(extraReward, extraCollateral);
_totalAmountReceived += (toDistribute + extraReward);
_vault.executeWhiskdrawal(msg.sender, toDistribute + extraReward);
unchecked {
++i;
}
}
}
Updates

Lead Judging Commences

shikhar229169 Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.