Summary
The claimNft
function lacks comprehensive input validation, performing only a basic zero-address check. This insufficient validation could lead to unexpected behaviors, contract state corruption, and potential exploits through malformed inputs or malicious contract interactions.
Vulnerability Detail
Current implementation shows minimal input validation:
function claimNft(address nftAddress) external {
if(nftAddress == address(0)) {
revert TokenDivider__NftAddressIsZero();
}
ERC20Info storage tokenInfo = nftToErc20Info[nftAddress];
}
Impact
Insufficient input validation could result in:
Interactions with non-existent or invalid NFT contracts
Processing of non-compliant ERC721 tokens
State corruption through invalid token IDs
Permanent locking of funds or tokens
DoS conditions through malformed inputs
Tool Used
Foundry
Proof of Concept
Invalid Contract Attack
contract InputValidationAttacker {
TokenDivider target;
function attackWithInvalidAddress() external {
address randomAddress = address(uint160(uint256(keccak256("random"))));
target.claimNft(randomAddress);
}
function attackWithNonCompliantContract() external {
NonCompliantContract malicious = new NonCompliantContract();
target.claimNft(address(malicious));
}
}
contract NonCompliantContract {
}
Attack Scenarios
function proveNonExistentContract() public {
address nonExistent = address(0x1);
target.claimNft(nonExistent);
}
function proveNonCompliantContract() public {
address nonCompliant = address(new NonCompliantContract());
target.claimNft(nonCompliant);
}
Recommended Mitigation Steps
Implement Comprehensive Input Validation
function claimNft(address nftAddress) external nonReentrant {
_validateClaimInputs(nftAddress);
_processClaim(nftAddress);
}
function _validateClaimInputs(address nftAddress) private view {
require(nftAddress != address(0), "Zero address");
uint256 size;
assembly {
size := extcodesize(nftAddress)
}
require(size > 0, "Not a contract");
require(
IERC165(nftAddress).supportsInterface(type(IERC721).interfaceId),
"Not ERC721"
);
ERC20Info storage tokenInfo = nftToErc20Info[nftAddress];
require(
tokenInfo.erc20Address != address(0),
"NFT not fractionalized"
);
require(
IERC721(nftAddress).ownerOf(tokenInfo.tokenId) == address(this),
"NFT not in contract"
);
}
Add Validation Modifiers
modifier validNFT(address nftAddress) {
require(nftAddress != address(0), "Zero address");
require(
nftToErc20Info[nftAddress].erc20Address != address(0),
"Not fractionalized"
);
_;
}
modifier validERC721(address nftAddress) {
require(nftAddress.code.length > 0, "Not a contract");
require(
IERC165(nftAddress).supportsInterface(type(IERC721).interfaceId),
"Not ERC721"
);
_;
}
Implement Safe Type Checks
library AddressValidator {
function isContract(address account) internal view returns (bool) {
uint256 size;
assembly {
size := extcodesize(account)
}
return size > 0;
}
function isERC721(address token) internal view returns (bool) {
try IERC165(token).supportsInterface(type(IERC721).interfaceId) returns (bool supported) {
return supported;
} catch {
return false;
}
}
}
Complete Implementation Example
contract TokenDivider {
using AddressValidator for address;
function claimNft(
address nftAddress
) external
nonReentrant
validNFT(nftAddress)
validERC721(nftAddress)
{
ERC20Info memory tokenInfo = _getValidatedTokenInfo(nftAddress);
_processClaim(nftAddress, tokenInfo);
}
}
Additional Recommendations
Add Input Boundaries
uint256 private constant MAX_TOKEN_ID = type(uint96).max;
uint256 private constant MAX_FRACTION_AMOUNT = 1e27;
Implement Error Collection
error InvalidNFTAddress(string reason);
error InvalidTokenID(uint256 tokenId);
error ValidationFailed(string reason);
Add Recovery Mechanisms
function recoverInvalidClaim(
address nftAddress,
address erc20Address
) external onlyOwner {
require(_isValidRecovery(nftAddress, erc20Address), "Invalid recovery");
_executeRecovery(nftAddress, erc20Address);
}