Summary
The transferErcTokens
function in the TokenDivider contract lacks validation to ensure that the provided NFT address has been properly fractionalized before proceeding with token transfers. This oversight could lead to operations on non-existent or invalid token states.
Vulnerability Detail
Current implementation:
function transferErcTokens(address nftAddress, address to, uint256 amount) external {
ERC20Info memory tokenInfo = nftToErc20Info[nftAddress];
balances[msg.sender][tokenInfo.erc20Address] -= amount;
balances[to][tokenInfo.erc20Address] += amount;
IERC20(tokenInfo.erc20Address).transferFrom(msg.sender, to, amount);
}
Impact
This vulnerability could result in:
Operations on non-existent ERC20 tokens
Silent failures in transfer operations
Inconsistent contract state
Loss of user funds through failed transfers
Potential system-wide state corruption
Tool Used
Foundry
Proof of Concept
contract NFTFractionalizationAttacker {
TokenDivider target;
constructor(address _target) {
target = TokenDivider(_target);
}
function exploit() external {
address randomNft = address(uint160(uint256(keccak256("random"))));
target.transferErcTokens(
randomNft,
address(this),
1000
);
}
}
contract ValidationTestSuite {
TokenDivider target;
function testUnfractionalizedTransfer() public {
MockNFT nft = new MockNFT();
try target.transferErcTokens(
address(nft),
address(1),
100
) {
fail("Should revert but didn't");
} catch {
}
}
}
Recommended Mitigation Steps
Add Fractionalization Status Validation
function _validateFractionalizationStatus(
address nftAddress
) private view returns (ERC20Info memory) {
ERC20Info memory tokenInfo = nftToErc20Info[nftAddress];
require(
tokenInfo.erc20Address != address(0),
"NFT not fractionalized"
);
require(
AddressUtils.isContract(tokenInfo.erc20Address),
"Invalid ERC20 contract"
);
require(
IERC165(tokenInfo.erc20Address).supportsInterface(type(IERC20).interfaceId),
"Invalid token implementation"
);
return tokenInfo;
}
Implement Status Tracking
contract TokenDivider {
mapping(address => bool) public isNFTFractionalized;
function divideNft(...) {
isNFTFractionalized[nftAddress] = true;
}
function claimNft(...) {
isNFTFractionalized[nftAddress] = false;
}
}
Complete Implementation Example
function transferErcTokens(
address nftAddress,
address to,
uint256 amount
) external nonReentrant {
_validateInputs(nftAddress, to, amount);
ERC20Info memory tokenInfo = _validateFractionalizationStatus(nftAddress);
require(
balances[msg.sender][tokenInfo.erc20Address] >= amount,
"Insufficient balance"
);
_updateBalances(msg.sender, to, tokenInfo.erc20Address, amount);
require(
IERC20(tokenInfo.erc20Address).transferFrom(msg.sender, to, amount),
"Transfer failed"
);
emit TokensTransferred(
msg.sender,
to,
tokenInfo.erc20Address,
amount
);
}
Add Recovery Mechanism
function recoverInvalidState(
address nftAddress
) external onlyOwner {
require(
!isNFTFractionalized[nftAddress],
"NFT is still fractionalized"
);
delete nftToErc20Info[nftAddress];
emit StateRecovered(nftAddress);
}
Additional Recommendations
Implement State Verification
function verifyFractionalization(
address nftAddress
) external view returns (bool, string memory) {
if (!isNFTFractionalized[nftAddress]) {
return (false, "Not fractionalized");
}
ERC20Info memory info = nftToErc20Info[nftAddress];
if (info.erc20Address == address(0)) {
return (false, "Invalid ERC20 address");
}
return (true, "Valid fractionalization");
}
Add Event Logging
event FractionalizationStatusChanged(
address indexed nftAddress,
bool status,
uint256 timestamp
);