Summary
The TSender protocol includes functionality for airdropping ERC20 tokens to a list of recipients. The tokens expected to be integrated are USDC, USDT, LINK and WETH (as indicated in the protocol readme file).
The airdropERC20()
function involves transferring the totalAmount
of tokens to the contract itself before redistributing them to the intended recipients. If the airdropped token are USDC
or USDT
, the implementation does not handle the scenarios where individual transfers to recipients may fail, specifically when dealing with blacklisted addresses. In case of blacklisted addresses, the ERC20 transfer function reverts, consequently reverting the airdropERC20
function and blocking the airdrop.
Vulnerability Details
The vulnerability arises from the lack of proper handling of failed transfer calls within the loop that distributes specific tokens (USDC and USDT) to recipients. Specifically, if the transfer call reverts, the function reverts and the airdrop is blocked.
Impact
The vulnerability arises from the lack of proper handling of failed transfer calls within the loop that distributes tokens to recipients. In the case of transferring USDC token or USDT token to a blacklisted address, the transfer function fails (ref. USDC smart contract https://etherscan.io/address/0x0882477e7895bdC5cea7cB1552ed914aB157Fe56#code#L561) and consequently either the airdropERC20
reverts. De facto the airdrop can't happen.
Considering a scenario where the sender intends to send the USDC (or USDT) calling the airdropERC20
function, a malicious recipient can block one of the addresses in the array from receiving USDC (or USDT) by adding it to the USDC (or USDT) blacklist (e.g. by doing something malicious with that address, etc.). This leads to the block of the airdrop.
In the TSenderHuffTest.t.sol, TSenderReferenceTest.t.sol, TSenderYulTest.t.sol, Gaslitetest.t.sol, TSenderHuffNoCheckTest.t.sol files add:
import {MockFalseTransferERC20} from "test/mocks/MockFalseTransferERC20.sol";
mockFalseTransferERC20 = new MockFalseTransferERC20();
Create a MockFalseTransferERC20.sol file and copy/paste this:
pragma solidity 0.8.24;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MockFalseTransferERC20 is ERC20 {
uint256 public constant MINT_AMOUNT = 1e18;
constructor() ERC20("Mock Token", "MT") {}
function mint() external {
_mint(msg.sender, MINT_AMOUNT);
}
function mint(uint256 amount) external {
_mint(msg.sender, amount);
}
function transfer(address to, uint256 value) public override returns (bool) {
if (to == address(1000)) {
revert("Transfer to blacklisted address not allowed");
} else {
address owner = _msgSender();
_transfer(owner, to, value);
return true;
}
}
}
In the Base_Test.t.sol add these:
import {MockFalseTransferERC20} from "test/mocks/MockFalseTransferERC20.sol";
MockFalseTransferERC20 public mockFalseTransferERC20;
function test_blacklistedAddressesBlockAirdrop() public virtual hasSafetyChecks {
address sender = makeAddr("sender");
uint256 amount = 123;
uint256 expectedTotalAmount = amount * 5;
vm.startPrank(sender);
mockFalseTransferERC20.mint(uint256(expectedTotalAmount));
mockFalseTransferERC20.approve(address(tSender), uint256(expectedTotalAmount));
vm.stopPrank();
address[] memory recipients = new address[](5);
recipients[0] = address(5);
recipients[1] = address(1000);
recipients[2] = address(11);
recipients[3] = address(6);
recipients[4] = address(13);
uint256[] memory amounts = new uint256[](5);
amounts[0] = amount;
amounts[1] = amount;
amounts[2] = amount;
amounts[3] = amount;
amounts[4] = amount;
vm.prank(sender);
vm.expectRevert();
tSender.airdropERC20(address(mockFalseTransferERC20), recipients, amounts, expectedTotalAmount);
assertEq(mockFalseTransferERC20.balanceOf(sender), amount * 5);
vm.stopPrank();
}
}
Run: forge test --match-test test_blacklistedAddressesBlockAirdrop -vv
Logs:
Ran 1 test for test/unit/noCheck/GasliteTest.t.sol:GasliteTest
[PASS] test_blacklistedAddressesBlockAirdrop() (gas: 2312)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 13.94ms (615.96µs CPU time)
Ran 1 test for test/unit/hasChecks/TSenderReferenceTest.t.sol:TSenderReferenceTest
[PASS] test_blacklistedAddressesBlockAirdrop() (gas: 115885)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 14.34ms (2.02ms CPU time)
Ran 1 test for test/unit/hasChecks/TSenderYulTest.t.sol:TSenderYulTest
[PASS] test_blacklistedAddressesBlockAirdrop() (gas: 114316)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 14.41ms (2.15ms CPU time)
Ran 1 test for test/unit/noCheck/TSenderHuffNoCheckTest.t.sol:TSenderHuffNoCheckTest
[PASS] test_blacklistedAddressesBlockAirdrop() (gas: 2312)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.11s (33.17µs CPU time)
Ran 1 test for test/unit/hasChecks/TSenderHuffTest.t.sol:TSenderHuffTest
[PASS] test_blacklistedAddressesBlockAirdrop() (gas: 113722)
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.11s (166.17µs CPU time)
Ran 5 test suites in 1.41s (2.26s CPU time): 5 tests passed, 0 failed, 0 skipped (5 total tests)
Tools Used
Manual review
Recommendations
Implement a logic that can handle blacklisted addresses.