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);
vm.startPrank(user);
IERC20(weth).approve(address(wethVault), AMOUNT);
kittyPool.depawsitMeowllateral(weth, AMOUNT);
kittyPool.meowintKittyCoin(AMOUNT * 1000);
vm.stopPrank();
vm.startPrank(liquidator);
IERC20(weth).approve(address(wethVault), AMOUNT);
kittyPool.depawsitMeowllateral(weth, AMOUNT);
kittyPool.meowintKittyCoin(AMOUNT * 1000);
vm.stopPrank();
ethUsdPriceFeedMock.updateAnswer(3000e8 / 2);
uint256 liquidatorBalanceBefore = kittyCoin.balanceOf(liquidator);
console.log("liquidator balance before: ", liquidatorBalanceBefore);
uint256 userBalanceBefore = kittyCoin.balanceOf(user);
console.log("User balance before: ", userBalanceBefore);
require(!kittyPool._hasEnoughMeowllateral(user), "User still has enough collateral");
vm.prank(liquidator);
uint256 totalAmountReceived = kittyPool.purrgeBadPawsition(user);
console.log("Total Amount Received by Liquidator: ", totalAmountReceived);
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;
}
}
}