Summary
The TSender
contract is designed to perform ERC20 token airdrops. However, it lacks mechanisms to recover funds (ETH or ERC20 tokens) that are accidentally sent to the contract. This is because the airdropERC20
function does not validate that the recipients array addresses is not the contract's address itself. This omission can result in a permanent loss of funds.
Vulnerability Details
Lack of Withdraw Mechanism:
The contract does not include any functions to withdraw ETH or ERC20 tokens that may be mistakenly sent to it. Funds sent to the contract remain locked indefinitely, as there is no way to retrieve them. This is because the airdropERC20
function does not validate that the recipient address is not the contract's address itself. Sending tokens to the contract's own address can lead to those tokens being permanently locked within the contract.
POC
Adding this to the EquivalenceTest.sol
contract:
function testCanSendTokenToAContractAddress() public {
uint128 totalAmountCapped = 4;
address sender = 0x0000000000000000000000000000000000000001;
uint256 modSeed = 100;
vm.assume(sender != address(0));
vm.assume(modSeed != 0);
uint256 totalAmount = uint256(totalAmountCapped);
uint256 numberOfRecipients = 4;
vm.startPrank(sender);
mockERC20.mint(totalAmount * AMOUNT_OF_COMPARING_CONTRACTS);
mockERC20.approve(address(yulTSender), totalAmount);
mockERC20.approve(address(huffTSender), totalAmount);
mockERC20.approve(address(solidityTSender), totalAmount);
vm.stopPrank();
address[] memory recipients = new address[](numberOfRecipients);
recipients[0] = address(yulTSender);
recipients[1] = address(huffTSender);
recipients[2] = address(solidityTSender);
recipients[3] = address(uint160(numberOfRecipients + 4));
uint256 amountLeft = totalAmount;
uint256[] memory amounts = new uint256[](numberOfRecipients);
amounts[0] = amountLeft % modSeed;
amountLeft -= amounts[0];
amounts[1] = amountLeft % modSeed;
amountLeft -= amounts[1];
amounts[2] = amountLeft % modSeed;
amountLeft -= amounts[2];
amounts[3] = amountLeft;
bytes4 selector = TSender.airdropERC20.selector;
bytes memory data = abi.encodeWithSelector(selector, address(mockERC20), recipients, amounts, totalAmount);
vm.startPrank(sender);
(bool succYul,) = address(yulTSender).call(data);
(bool succHuff,) = address(huffTSender).call(data);
(bool succSolidity,) = address(solidityTSender).call(data);
vm.stopPrank();
assert(succYul == succHuff);
assert(succYul == succSolidity);
assert(mockERC20.balanceOf(recipients[0]) == amounts[0] * AMOUNT_OF_COMPARING_CONTRACTS);
assert(mockERC20.balanceOf(recipients[1]) == amounts[1] * AMOUNT_OF_COMPARING_CONTRACTS);
assert(mockERC20.balanceOf(recipients[2]) == amounts[2] * AMOUNT_OF_COMPARING_CONTRACTS);
assert(mockERC20.balanceOf(recipients[3]) == amounts[3] * AMOUNT_OF_COMPARING_CONTRACTS);
}
You will see that this actually passes, which means that we successful sent tokens to the contract's own address which cannot be retreive.
Impact
Permanent Loss of Funds: Any ETH or ERC20 tokens sent to the contract mistakenly cannot be retrieved, leading to a permanent loss of those assets.
Locked Tokens: If the contract address is included in the recipient list for airdrops, those tokens will be locked and irretrievable.
Tools Used
Recommendations
Enhance Recipient Validation:
Tsender.sol file:
function airdropERC20(
address tokenAddress,
address[] calldata recipients,
uint256[] calldata amounts,
uint256 totalAmount
) external {
assembly {
// Existing setup code...
let end := add(recipients.offset, shl(5, recipients.length))
let diff := sub(recipients.offset, amounts.offset)
let addedAmount := 0
for { let addressOffset := recipients.offset } 1 {} {
let recipient := calldataload(addressOffset)
// Check to address
if iszero(recipient) {
mstore(0x00, 0x1647bca2) // cast sig "TSender__ZeroAddress()"
revert(0x1c, 0x04)
}
// Check if recipient is a contract address
+ if iszero(iszero(extcodesize(recipient))) {
+ mstore(0x00, 0x63b62563) // cast sig "TSender__RecipientIsContract()"
+ revert(0x1c, 0x04)
}
// Existing transfer logic...
// increment the address offset
addressOffset := add(addressOffset, 0x20)
// if addressOffset >= end, break
if iszero(lt(addressOffset, end)) { break }
}
// Existing check totals logic...
}
}
Tsender.huff file
#define macro AIRDROP_ERC20() = takes (0) returns (0) {
// Existing setup...
loop_start:
// Existing loop setup...
// Check for zero address
dup1 0x00 eq zero_address_to jumpi
// Check if recipient is a contract
+ extcodesize
+ dup1
+ iszero iszero jumpi
// cast sig "TSender__RecipientIsContract()"
+ 0x63b62563 0x00 mstore
+ 0x04 [TWENTY_EIGHT] revert
// Existing memory setup for transfer...
transfer_didnt_fail:
0x20 add
dup2 dup2
lt loop_start jumpi
// Existing total amount check...
}