Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Users may lose airdropped tokens in ERC20Airdrop2 contract #241

Closed
c4-bot-6 opened this issue Mar 26, 2024 · 3 comments
Closed

Users may lose airdropped tokens in ERC20Airdrop2 contract #241

c4-bot-6 opened this issue Mar 26, 2024 · 3 comments
Labels
3 (High Risk) Assets can be stolen/lost/compromised directly bug Something isn't working duplicate-245 🤖_51_group AI based duplicate group recommendation satisfactory satisfies C4 submission criteria; eligible for awards upgraded by judge Original issue severity upgraded from QA/Gas by judge

Comments

@c4-bot-6
Copy link
Contributor

Lines of code

https://github.com/code-423n4/2024-03-taiko/blob/main/packages/protocol/contracts/team/airdrop/ERC20Airdrop2.sol#L39-L44
https://github.com/code-423n4/2024-03-taiko/blob/main/packages/protocol/contracts/team/airdrop/ERC20Airdrop2.sol#L88-L94
https://github.com/code-423n4/2024-03-taiko/blob/main/packages/protocol/contracts/team/airdrop/ERC20Airdrop2.sol#L114-L120

Vulnerability details

Issue Description

During an airdrop organized with the ERC20Airdrop2 contract, there are two steps involved: first, the chosen users must claim their airdrop amounts by calling the claim function, and then they must wait until the withdrawal period begins to start withdrawing those amounts using the withdraw function.

The withdrawal period operates similarly to a vesting period, during which initially block.timestamp < claimEnd, no amount can be withdrawn. Subsequently, users can gradually withdraw as time passes until the moment block.timestamp == claimEnd + withdrawalWindow, when the entire amount becomes withdrawable.

In typical vesting scenarios, the full airdropped amount remains withdrawable after the withdrawal period has ended. However, in the case of ERC20Airdrop2, this is not the case due to the presence of the ongoingWithdrawals modifier within the withdraw function:

function withdraw(address user) external ongoingWithdrawals {
    (, uint256 amount) = getBalance(user);
    withdrawnAmount[user] += amount;
    IERC20(token).transferFrom(vault, user, amount);

    emit Withdrawn(user, amount);
}

The ongoingWithdrawals modifier allows users to withdraw only during the withdrawal period, defined as claimEnd <= block.timestamp <= claimEnd + withdrawalWindow, as illustrated in the code below:

modifier ongoingWithdrawals() {
    if (claimEnd > block.timestamp || claimEnd + withdrawalWindow < block.timestamp) {
        revert WITHDRAWALS_NOT_ONGOING();
    }
    _;
}

It's important to note that the full airdropped amount becomes withdrawable only when block.timestamp == claimEnd + withdrawalWindow, as enforced by the getBalance function invoked within the withdraw function:

function getBalance(address user)
    public
    view
    returns (uint256 balance, uint256 withdrawableAmount)
{
    balance = claimedAmount[user];
    // If balance is 0 then there is no balance and withdrawable amount
    if (balance == 0) return (0, 0);
    // Balance might be positive before end of claiming (claimEnd - if claimed already) but
    // withdrawable is 0.
    if (block.timestamp < claimEnd) return (balance, 0);

    // Hard cap timestamp - so range cannot go over - to get more allocation over time.
    uint256 timeBasedAllowance = balance
        * (block.timestamp.min(claimEnd + withdrawalWindow) - claimEnd) / withdrawalWindow;

    withdrawableAmount = timeBasedAllowance - withdrawnAmount[user];
}

This setup can lead to users losing a part or all of their airdropped tokens.

Let's consider two scenarios to illustrate this issue:

- Scenario 1 (relatively low impact):

  • Bob was airdropped 1000 tokens which he claimed using the claim function.

  • After 80% of the withdrawal period (withdrawalWindow) has passed, Bob decides to withdraw some of his airdropped tokens.

  • According to getBalance, he should receive 80% of his total airdropped amount (the maximum withdrawable amount at that time), so Bob receives 800 tokens.

  • When the withdrawal period (withdrawalWindow) is about to end, Bob submits a transaction to withdraw the remaining balance of his airdrop, 1 hour before the end (claimEnd + withdrawalWindow).

  • However, Bob's transaction remains pending for more than 1 hour, and when it finally gets executed, the withdrawal period has ended, causing Bob's withdraw call to revert.

  • As ongoingWithdrawals only allows withdrawals during the specified period, Bob will not be able to call withdraw again, resulting in the loss of his remaining 200 airdropped tokens.

- Scenario 2 (higher impact):

  • Alice was airdropped 1000 tokens which she claimed using the claim function.

  • Alice chooses not to withdraw until the last moment (the end of the withdrawal period) to receive her full airdropped amount.

  • As the withdrawal period (withdrawalWindow) is about to end, Alice submits a transaction to withdraw the remaining balance of her airdrop, 1 hour before the period ends (claimEnd + withdrawalWindow).

  • However, Alice's transaction remains pending for more than 1 hour, and when it finally gets executed, the withdrawal period has ended, causing Alice's withdraw call to revert.

  • As ongoingWithdrawals only allows withdrawals during the specified period, Alice will not be able to call withdraw again, resulting in the loss of all her airdropped tokens.

In both scenarios, users risk losing a portion or all of their airdropped tokens due to the unpredictability of transaction execution timestamps (block.timestamp). And this aren't exceptional scenarios as this is expected to happen to many other users because the tx execution timestamp block.timestamp is not the one in which the tx is submitted but rather the time of the block in which the tx is included which isn't always predictable as tx can stay pending in the mempool for many hours (or even days).

And the fact that the full airdropped amount is only withdrawable at a specific timestamp claimEnd + withdrawalWindow makes the chances of this issue occurring even more frequent as users will want to get the maximum amount of tokens and end up losing some of it

Impact

Users may lose a portion or all their airdropped amounts because of the withdrawable period.

Tools Used

Manual review, VS Code

Recommended Mitigation

One possible solution to address this issue is to remove the ongoingWithdrawals modifier from the withdraw function. This modification would allow users to withdraw their airdropped tokens at any time, mitigating the risk of loss due to transaction execution timing.

Assessed type

Context

@c4-bot-6 c4-bot-6 added 2 (Med Risk) Assets not at direct risk, but function/availability of the protocol could be impacted or leak value bug Something isn't working labels Mar 26, 2024
c4-bot-4 added a commit that referenced this issue Mar 26, 2024
@c4-bot-13 c4-bot-13 added the 🤖_51_group AI based duplicate group recommendation label Mar 27, 2024
@c4-pre-sort
Copy link

minhquanym marked the issue as duplicate of #245

@c4-judge
Copy link
Contributor

0xean changed the severity to 3 (High Risk)

@c4-judge c4-judge added 3 (High Risk) Assets can be stolen/lost/compromised directly upgraded by judge Original issue severity upgraded from QA/Gas by judge and removed 2 (Med Risk) Assets not at direct risk, but function/availability of the protocol could be impacted or leak value labels Apr 10, 2024
@c4-judge
Copy link
Contributor

0xean marked the issue as satisfactory

@c4-judge c4-judge added the satisfactory satisfies C4 submission criteria; eligible for awards label Apr 10, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3 (High Risk) Assets can be stolen/lost/compromised directly bug Something isn't working duplicate-245 🤖_51_group AI based duplicate group recommendation satisfactory satisfies C4 submission criteria; eligible for awards upgraded by judge Original issue severity upgraded from QA/Gas by judge
Projects
None yet
Development

No branches or pull requests

4 participants