Pieces Protocol

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

## Uncontrolled External Contract Interactions in TokenDivider's NFT Fractionalization Process

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 {
// EXTERNAL CALL SET #1: NFT Contract Queries
string memory nftName = ERC721(nftAddress).name(); // External Call
string memory nftSymbol = ERC721(nftAddress).symbol(); // External Call
// EXTERNAL CALL SET #2: New ERC20 Creation & Minting
ERC20ToGenerateNftFraccion erc20Contract = new ERC20ToGenerateNftFraccion(
string(abi.encodePacked(nftName, "Fraccion")),
string(abi.encodePacked("F", nftSymbol))
);
erc20Contract.mint(address(this), amount); // External Call
// EXTERNAL CALL SET #3: NFT Transfer
IERC721(nftAddress).safeTransferFrom( // External Call
msg.sender,
address(this),
tokenId
);
// State updates happen here...
// EXTERNAL CALL SET #4: ERC20 Transfer
IERC20(erc20).transfer(msg.sender, amount); // External Call
}

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 {
// Malicious implementation that manipulates state or reverts selectively
if (callCount > 1) revert("Selective failure");
// Continue with transfer logic...
}
function ownerOf(uint256) external pure returns (address) {
return address(this);
}
}

Attack Simulation

  1. DoS Attack Through Reverting Calls

contract DoSAttacker {
TokenDivider target;
MaliciousNFT malNFT;
function setupAttack() external {
malNFT = new MaliciousNFT();
// Initial call succeeds, subsequent calls fail
target.divideNft(address(malNFT), 1, 1000);
}
}
  1. Gas Griefing Attack

contract GasGriefingNFT is IERC721 {
function safeTransferFrom(address from, address to, uint256 tokenId) external {
// Consume massive gas through loop
for(uint i = 0; i < 1000; i++) {
// Expensive operations
keccak256(abi.encode(i));
}
}
}
  1. State Manipulation Attack

contract StateManipulatorNFT is IERC721 {
TokenDivider target;
function safeTransferFrom(address from, address to, uint256 tokenId) external {
// Attempt to manipulate contract state during transfer
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 {
// Cache initial state
address initialOwner = msg.sender;
// Validate NFT contract
_validateNFTContract(nftAddress);
// Safe external calls with try-catch
(string memory nftName, string memory nftSymbol) = _safeNFTQuery(nftAddress);
// Create ERC20 with validation
ERC20ToGenerateNftFraccion erc20Contract = _createERC20SafelyWithValidation(
nftName,
nftSymbol
);
// Update state before external transfers
_updateStateForDivision(nftAddress, tokenId, address(erc20Contract), amount);
// Execute transfers with rollback capability
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 {
// Set maximum gas for external calls
uint256 gasLimit = 50000;
// NFT Transfer with gas limit
(bool success, ) = address(nftAddress).call{gas: gasLimit}(
abi.encodeWithSelector(
IERC721.safeTransferFrom.selector,
msg.sender,
address(this),
tokenId
)
);
require(success, "NFT transfer failed");
// Safe ERC20 transfer
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:

  1. Implement contract whitelisting for known-good NFT contracts

  2. Add gas estimation before external calls

  3. Implement emergency shutdown mechanism

  4. Add event logging for all external calls

  5. Consider using proxy patterns for upgradeable contracts

Updates

Lead Judging Commences

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

Support

FAQs

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