Summary
The claimNft
function in the TokenDivider contract performs state updates after making external contract calls, violating the checks-effects-interactions pattern. This sequence creates a critical vulnerability where the contract's state could become inconsistent if external calls fail or are manipulated.
Vulnerability Detail
Current implementation shows state updates occurring after external contract interactions:
function claimNft(address nftAddress) external {
ERC20Info storage tokenInfo = nftToErc20Info[nftAddress];
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
This vulnerability could result in:
Inconsistent contract state if external calls fail
Loss of user tokens if state updates persist after failed transfers
Potential double-spending vulnerabilities
Permanent locking of NFTs in the contract
Manipulation of contract state through malicious external contracts
Tool Used
Foundry
Proof of Concept
contract StateManipulationAttacker {
TokenDivider target;
bool shouldRevert;
constructor(address _target) {
target = TokenDivider(_target);
}
function attackClaim(address nftAddress) external {
shouldRevert = true;
target.claimNft(nftAddress);
}
function onERC721Received(
address,
address,
uint256,
bytes calldata
) external returns (bytes4) {
if(shouldRevert) {
revert("Malicious revert");
}
return this.onERC721Received.selector;
}
}
Attack Scenario
User calls claimNft
with valid parameters
ERC20 tokens are successfully burned
Contract updates state variables
During NFT transfer, the receiving contract reverts
The function call fails, but state updates remain:
ERC20 tokens are burned
State indicates the claim is complete
NFT remains locked in the contract
User loses their tokens with no recourse
Recommended Mitigation Steps
Implement Checks-Effects-Interactions Pattern
function claimNft(
address nftAddress
) external nonReentrant {
ERC20Info storage tokenInfo = _validateClaimRequest(nftAddress);
uint256 totalSupply = erc20ToMintedAmount[tokenInfo.erc20Address];
_updateStateForClaim(nftAddress, tokenInfo.erc20Address);
_executeClaimTransfers(tokenInfo, totalSupply);
}
Add State Management Functions
function _validateClaimRequest(
address nftAddress
) private view returns (ERC20Info storage) {
ERC20Info storage tokenInfo = nftToErc20Info[nftAddress];
require(tokenInfo.erc20Address != address(0), "NFT not fractionalized");
uint256 totalSupply = erc20ToMintedAmount[tokenInfo.erc20Address];
require(
balances[msg.sender][tokenInfo.erc20Address] == totalSupply,
"Insufficient balance"
);
return tokenInfo;
}
function _updateStateForClaim(
address nftAddress,
address erc20Address
) private {
uint256 totalSupply = erc20ToMintedAmount[erc20Address];
delete balances[msg.sender][erc20Address];
delete erc20ToMintedAmount[erc20Address];
delete erc20ToNft[erc20Address];
delete nftToErc20Info[nftAddress];
emit StateUpdated(nftAddress, erc20Address, totalSupply);
}
function _executeClaimTransfers(
ERC20Info memory tokenInfo,
uint256 totalSupply
) private {
bool burnSuccess = ERC20ToGenerateNftFraccion(tokenInfo.erc20Address)
.burnFrom(msg.sender, totalSupply);
require(burnSuccess, "Burn failed");
IERC721(nftAddress).safeTransferFrom(
address(this),
msg.sender,
tokenInfo.tokenId
);
emit NftClaimed(nftAddress, msg.sender, tokenInfo.tokenId);
}
Implement Transaction Guards
contract TokenDivider {
mapping(bytes32 => bool) private _pendingTx;
modifier guardedTransaction(bytes32 txId) {
require(!_pendingTx[txId], "Transaction in progress");
_pendingTx[txId] = true;
_;
delete _pendingTx[txId];
}
}
Add Recovery Mechanism
function recoverFailedClaim(
address nftAddress,
address erc20Address
) external onlyOwner {
require(
IERC721(nftAddress).ownerOf(tokenInfo.tokenId) == address(this),
"NFT not in contract"
);
_restoreClaimState(nftAddress, erc20Address);
}
Additional Recommendations
Implement comprehensive event logging
Add emergency pause functionality
Consider using a state machine pattern
Implement claim timeouts
Add circuit breaker pattern for repeated failures