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 {
if iszero(eq(recipients.length, amounts.length)) {
mstore(0x00, 0x50a302d6)
revert(0x1c, 0x04)
}
mstore(0x00, hex"23b872dd")
mstore(0x04, caller())
mstore(0x24, address())
mstore(0x44, totalAmount)
if iszero(call(gas(), tokenAddress, 0, 0x00, 0x64, 0, 0)) {
mstore(0x00, 0xfa10ea06)
revert(0x1c, 0x04)
}
mstore(0x00, hex"a9059cbb")
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)
if iszero(recipient) {
mstore(0x00, 0x1647bca2)
revert(0x1c, 0x04)
}
mstore(0x04, recipient)
mstore(0x24, calldataload(sub(addressOffset, diff)))
addedAmount := add(addedAmount, mload(0x24))
if iszero(call(gas(), tokenAddress, 0, 0x00, 0x44, 0, 0)) {
mstore(0x00, 0xfa10ea06)
revert(0x1c, 0x04)
}
addressOffset := add(addressOffset, 0x20)
if iszero(lt(addressOffset, end)) { break }
}
if iszero(eq(addedAmount, totalAmount)) {
mstore(0x00, 0x63b62563)
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:
Perform the required checks and validations.(Checks)
Update contract's internal state.(Effects)
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:
Follow the checks-effects-interactions pattern.
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 {
if iszero(eq(recipients.length, amounts.length)) {
mstore(0x00, "TSender__LengthsDontMatch")
revert(0x1c, 0x04)
}
if iszero(tokenAddress) {
mstore(0x00, "TSender__InvalidTokenAddress")
revert(0x1c, 0x04)
}
mstore(0x00, 0x23b872dd)
mstore(0x04, caller())
mstore(0x24, address())
mstore(0x44, totalAmount)
if iszero(call(gas(), tokenAddress, 0, 0x00, 0x64, 0, 0)) {
mstore(0x00, "TSender__TransferFailed")
revert(0x1c, 0x04)
}
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))
if iszero(recipient) {
mstore(0x00, 0x1647bca2)
revert(0x1c, 0x04)
}
if iszero(amount) {
mstore(0x00, "TSender__ZeroAmount")
revert(0x1c, 0x04)
}
addedAmount := add(addedAmount, amount)
if gt(addedAmount, totalAmount) {
mstore(0x00, 0x63b62563)
revert(0x1c, 0x04)
}
mstore(0x00, 0xa9059cbb)
mstore(0x04, recipient)
mstore(0x24, amount)
if iszero(call(gas(), tokenAddress, 0, 0x00, 0x44, 0, 0)) {
mstore(0x00, 0xfa10ea06)
revert(0x1c, 0x04)
}
}
if iszero(eq(addedAmount, totalAmount)) {
mstore(0x00, "TSender__TotalDoesntAddUp")
revert(0x1c, 0x04)
}
}
}