TSender

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

Reentrancy in `Tsender.sol__airdropERC20` function

Summary

The TSender.sol__airdropERC20 function is vulnerable to reentract attack due to not following Checks-Effects-Interactions pattern. The transferFrom and transfer functions make call before updating the state of the contract, allowing a malicious contract to re-enter the function and potentially drain the tokens.

Vulnerability Details

Problematic code

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)
}
// transferFrom(address from, address to, uint256 amount)
// cast sig "transferFrom(address,address,uint256)"
// This will result in memory looking like this:
// 0x00: 0x23b872dd00000000000000000000000000000000000000000000000000000000
mstore(0x00, hex"23b872dd")// function selector stored at 0x00 - transferFrom()
// from address
mstore(0x04, caller()) // from address, passing the first argument of the transferFrom function
// to address (this contract)
mstore(0x24, address())
// total amount
mstore(0x44, totalAmount)
if iszero(call(gas(), tokenAddress, 0, 0x00, 0x64, 0, 0)) {
mstore(0x00, 0xfa10ea06) // cast sig "TSender__TransferFailed()"
revert(0x1c, 0x04)
}
// transfer(address to, uint256 value)
mstore(0x00, hex"a9059cbb")// hex"a9059cbb"- function selector for transfer()
// end of array
// recipients.offset actually points to the recipients.length offset, not the first address of the array offset
let end := add(recipients.offset, shl(5, recipients.length)) //add
let diff := sub(recipients.offset, amounts.offset)//subtract
// Checking totals at the end
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)
}
// to address
mstore(0x04, recipient)
// amount
mstore(0x24, calldataload(sub(addressOffset, diff)))
// Keep track of the total amount
addedAmount := add(addedAmount, mload(0x24))
// transfer the tokens
if iszero(call(gas(), tokenAddress, 0, 0x00, 0x44, 0, 0)) {
mstore(0x00, 0xfa10ea06) // cast sig "TSender__TransferFailed()"
revert(0x1c, 0x04)
}
// increment the address offset
addressOffset := add(addressOffset, 0x20)
// if addressOffset >= end, break
if iszero(lt(addressOffset, end)) { break }
}
// Check if the totals match
if iszero(eq(addedAmount, totalAmount)) {
mstore(0x00, 0x63b62563) // cast sig TSender__TotalDoesntAddUp()
revert(0x1c, 0x04)
}
}
}

Reentrancy attacks occur when smart contracts make an external call before updating the state variable and the external contract is able to call back and perform certain actions. This can lead to unexpected behavior and potential exploitation, such as the draining of funds or tokens from the contract.

In the case of the Tsender.sol__airdropERc20 function, the vulnerability arises due to not following of checks-effects-interactions pattern. This pattern suggests that a contract should:

  1. Perform the required checks and validations.(Checks)

  2. Update contract's internal state.(Effects)

  3. Interact with external contract or accounts.(Interactions)

In the Tsender.sol__airdropERC20 function the order is reversed. The function calls transferFrom function of the ERC20 token contract to transfer the totalAmount of token from the caller's address to the contract's address. Then, it enters a loop where it calls the transfer function of the ERC20 token contract to transfer tokens from the contract's address to the recipient's address.

This means that the function first interacts with the external contract and then updates the state variable. Hence, if a recipient's address is a malicious contract, it can re-enter the Tsender.sol__airdropERC20 function during the execution of the transfer call. It could potentially allow the malicious contract to drain all the tokens from the contract or disrupt the airdrop logic.

Impact

This is an example of a reentrancy attack, it could potentially allow the malicious contract to drain all the tokens/funds from the contract or disrupt the airdrop logic.

Tools Used

Forge

Recommendations

The recommended mitigation:

  1. Follow the checks-effects-interactions pattern.

  2. Use the Reentracyguard modifier by OpenZeppelin.

Re-written Tsender.sol__airdropERC20 function by following CEI pattern and using ReentrancyGuard by OpenZeppelin

function airdropERC20(
address tokenAddress,
address[] calldata recipients,
uint256[] calldata amounts,
uint256 totalAmount
) external nonReentrant {
assembly {
// Check that the lengths of the recipients and amounts arrays match
if iszero(eq(recipients.length, amounts.length)) {
mstore(0x00, "TSender__LengthsDontMatch")
revert(0x1c, 0x04)
}
// Check that the token address is not zero
if iszero(tokenAddress) {
mstore(0x00, "TSender__InvalidTokenAddress")
revert(0x1c, 0x04)
}
// Prepare the selector for transferFrom(address, address, uint256)
mstore(0x00, 0x23b872dd)
// Set the from address (msg.sender)
mstore(0x04, caller())
// Set the to address (this contract)
mstore(0x24, address())
// Set the total amount
mstore(0x44, totalAmount)
// Call transferFrom(tokenAddress, msg.sender, this, totalAmount)
if iszero(call(gas(), tokenAddress, 0, 0x00, 0x64, 0, 0)) {
mstore(0x00, "TSender__TransferFailed")
revert(0x1c, 0x04)
}
// Calculate the end of the recipients array
let end := add(recipients.offset, shl(5, recipients.length))
let diff := sub(recipients.offset, amounts.offset)
let addedAmount := 0
for {
let offset := recipients.offset
} lt(offset, end) {
offset := add(offset, 0x20)
} {
let recipient := calldataload(offset)
let amount := calldataload(sub(offset, diff))
// Check that the recipient address is not zero
if iszero(recipient) {
mstore(0x00, 0x1647bca2)
revert(0x1c, 0x04)
}
// Check that the amount is greater than zero
if iszero(amount) {
mstore(0x00, "TSender__ZeroAmount")
revert(0x1c, 0x04)
}
// Update the added amount and check for overflow
addedAmount := add(addedAmount, amount)
if gt(addedAmount, totalAmount) {
mstore(0x00, 0x63b62563)
revert(0x1c, 0x04)
}
// Prepare the selector for transfer(address, uint256)
mstore(0x00, 0xa9059cbb)
// Set the to address (recipient)
mstore(0x04, recipient)
// Set the value (amount)
mstore(0x24, amount)
// Call transfer(tokenAddress, recipient, amount)
if iszero(call(gas(), tokenAddress, 0, 0x00, 0x44, 0, 0)) {
mstore(0x00, 0xfa10ea06)
revert(0x1c, 0x04)
}
}
// Check that the added amount equals the total amount
if iszero(eq(addedAmount, totalAmount)) {
mstore(0x00, "TSender__TotalDoesntAddUp")
revert(0x1c, 0x04)
}
}
}
Updates

Lead Judging Commences

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

Support

FAQs

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