TSender

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

[H-01] Duplicate Address Vulnerability in `airdropERC20` Function of `TSender` Contract

Summary

The TSender contract includes an airdropERC20 function designed to distribute ERC20 tokens to multiple recipients. While the contract has an areListsValid function to check for duplicate addresses, zero addresses, and mismatched array lengths, it does not enforce the duplicate address check within the airdropERC20 function itself. This oversight allows users to bypass the areListsValid function and call airdropERC20 directly with multiple instances of the same address, claiming more tokens than intended.

Vulnerability Details

The airdropERC20 function does not check for duplicate addresses within the recipients array. The validation for duplicate addresses is only present in the areListsValid function, which can be bypassed by calling airdropERC20 directly.

Impact

If users exploit this vulnerability by providing multiple instances of the same address in the recipients array, they can:

  • Claim More Tokens: Receive more tokens than intended, leading to unfair distribution and depletion of the token pool.

  • Bypass Validation: Avoid the checks enforced in the areListsValid function, leading to incorrect and potentially malicious behavior in the airdrop process.

Tools Used

  • Manual Review

Recommendations

To address this issue, implement a duplicate address check directly within the airdropERC20 function to ensure that no recipient receives multiple airdrops. This can be done efficiently using a mapping to track seen addresses.

Revised Implementation in Yul

Here is the revised implementation of the airdropERC20 function with an added duplicate address check:

function airdropERC20(
address tokenAddress,
address[] calldata recipients,
uint256[] calldata amounts,
uint256 totalAmount
) external {
assembly {
// Check for equal lengths
if iszero(eq(recipients.length, amounts.length)) {
mstore(0x00, 0x50a302d6) // cast sig TSender__LengthsDontMatch()
revert(0x1c, 0x04)
}
// Initialize total amount check and seen addresses mapping
let addedAmount := 0
let seen := mload(0x40) // free memory pointer for seen addresses
// Pre-transfer validation: Calculate the actual total amount
let end := add(recipients.offset, shl(5, recipients.length))
let diff := sub(recipients.offset, amounts.offset)
for { let addressOffset := recipients.offset } lt(addressOffset, end) { addressOffset := add(addressOffset, 0x20) } {
let recipient := calldataload(addressOffset)
// Check for zero address
if iszero(recipient) {
mstore(0x00, 0x1647bca2) // cast sig "TSender__ZeroAddress()"
revert(0x1c, 0x04)
}
// Check for duplicate address
let seenSlot := add(seen, recipient)
if sload(seenSlot) {
mstore(0x00, 0xd5cf8771) // cast sig "TSender__DuplicateAddress()"
revert(0x1c, 0x04)
}
sstore(seenSlot, 1)
// Calculate total amount
addedAmount := add(addedAmount, calldataload(sub(addressOffset, diff)))
}
// Check if the totals match
if iszero(eq(addedAmount, totalAmount)) {
mstore(0x00, 0x63b62563) // cast sig TSender__TotalDoesntAddUp()
revert(0x1c, 0x04)
}
// Perform transferFrom to the contract
mstore(0x00, hex"23b872dd") // transferFrom(address,address,uint256) function selector
mstore(0x04, caller())
mstore(0x24, address())
mstore(0x44, totalAmount)
let success := call(gas(), tokenAddress, 0, 0x00, 0x64, 0, 0)
if iszero(success) {
mstore(0x00, 0xfa10ea06) // cast sig "TSender__TransferFailed()"
revert(0x1c, 0x04)
}
// Perform transfers to recipients
mstore(0x00, hex"a9059cbb") // transfer(address,uint256) function selector
for { let addressOffset := recipients.offset } lt(addressOffset, end) { addressOffset := add(addressOffset, 0x20) } {
let recipient := calldataload(addressOffset)
// to address
mstore(0x04, recipient)
// amount
mstore(0x24, calldataload(sub(addressOffset, diff)))
// Transfer the tokens
let transferSuccess := call(gas(), tokenAddress, 0, 0x00, 0x44, 0, 0)
if iszero(transferSuccess) {
mstore(0x00, 0xfa10ea06) // cast sig "TSender__TransferFailed()"
revert(0x1c, 0x04)
}
// Check if the transfer returned a boolean value and ensure it is true
switch returndatasize()
case 0 {
// If no data is returned, assume the transfer was successful
}
case 0x20 {
returndatacopy(0x00, 0x00, 0x20)
if iszero(mload(0x00)) {
mstore(0x00, 0xfa10ea06) // cast sig "TSender__TransferFailed()"
revert(0x1c, 0x04)
}
}
default {
mstore(0x00, 0xfa10ea06) // cast sig "TSender__TransferFailed()"
revert(0x1c, 0x04)
}
}
}
}
Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Design choice
maanvad3r Submitter
about 1 year ago
inallhonesty Lead Judge
about 1 year ago
inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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