TSender

Cyfrin
DeFiFoundry
15,000 USDC
View results
Submission Details
Severity: medium
Invalid

Permanent Locking of Funds and Absence of Contract Address Check in TSender Contract

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

  1. 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;
// Arrange
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);
// Act
vm.startPrank(sender);
(bool succYul,) = address(yulTSender).call(data);
(bool succHuff,) = address(huffTSender).call(data);
(bool succSolidity,) = address(solidityTSender).call(data);
vm.stopPrank();
// Assert
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

  • Manual Code Review:

Recommendations

  1. Enhance Recipient Validation:

    • Modify the airdropERC20 or the areListsValid function to include checks that prevent the contract address from being included in the recipient list. This can be done by adding this to the airdropERC20 function in the

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...
}
Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Other

Support

FAQs

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