Summary
The TokenDivider
contract lacks a mechanism to cancel sell orders. Once a user creates a sell order using sellErc20()
, their tokens are transferred to the contract and can become permanently trapped if no one buys the order. The only way to recover the tokens is through a successful sale, which may never happen.
Vulnerability Details
When a user creates a sell order:
Their tokens are deducted from their balance
Tokens are transferred to the contract
No mechanism exists to cancel the order and recover tokens
Tokens remain locked until someone purchases the order
This creates a scenario where user assets can become permanently inaccessible if their sell order is not attractive to buyers or if the market conditions change.
Impact
HIGH: Potential permanent loss of user assets
Users cannot recover their tokens if their sell order doesn't attract buyers
Forces users to keep unfavorable sell orders active
No way to respond to changing market conditions
Proof of Concept
function testTokensCanBePermanentlyTrapped() public {
vm.startPrank(USER);
erc721Mock.approve(address(tokenDivider), TOKEN_ID);
tokenDivider.divideNft(address(erc721Mock), TOKEN_ID, AMOUNT);
ERC20Mock erc20Mock = ERC20Mock(tokenDivider.getErc20InfoFromNft(address(erc721Mock)).erc20Address);
erc20Mock.approve(address(tokenDivider), AMOUNT);
uint256 highPrice = 1000 ether;
tokenDivider.sellErc20(address(erc721Mock), highPrice, AMOUNT);
assertEq(erc20Mock.balanceOf(address(tokenDivider)), AMOUNT);
assertEq(tokenDivider.getBalanceOf(USER, address(erc20Mock)), 0);
vm.expectRevert();
tokenDivider.transferErcTokens(address(erc721Mock), USER2, AMOUNT);
vm.expectRevert(TokenDivider.TokenDivider__NotEnoughErc20Balance.selector);
tokenDivider.claimNft(address(erc721Mock));
vm.stopPrank();
}
Tools Used
Manual review
Foundry for POC
Recommendations
Implement a cancelSellOrder
function:
event OrderCancelled(
address indexed seller,
address indexed erc20Address,
uint256 amount
);
function cancelSellOrder(uint256 orderIndex) external {
SellOrder[] storage orders = s_userToSellOrders[msg.sender];
require(orderIndex < orders.length, "Invalid order index");
SellOrder memory orderToCancel = orders[orderIndex];
require(orderToCancel.seller == msg.sender, "Not order owner");
balances[msg.sender][orderToCancel.erc20Address] += orderToCancel.amount;
orders[orderIndex] = orders[orders.length - 1];
orders.pop();
IERC20(orderToCancel.erc20Address).transfer(msg.sender, orderToCancel.amount);
emit OrderCancelled(msg.sender, orderToCancel.erc20Address, orderToCancel.amount);
}
Consider adding a time limit on orders after which they can be automatically cancelled.