31552 - [SC - Insight] Lack of the validation for a Flash token protec...

Submitted on May 21st 2024 at 08:14:05 UTC by @Oxmuxyz for Boost | Alchemix

Report ID: #31552

Report type: Smart Contract

Report severity: Insight

Target: https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/VotingEscrow.sol

Impacts:

  • Manipulation of governance voting result deviating from voted outcome and resulting in a direct change from intended effect of original results

Description

Brief/Intro

There is no the validation for a Flash token protection in the VotingEscrow#getVotes(), which potentially allow a malicious actor to launch a Flash token attack to manipulate the governance voting result.

Vulnerability Details

Within the VotingEscrow contract, the ownershipChange storage would be defined to set the block number for a tokenId of veALCX when there is an ownership change like this: https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L82

    /// @notice Sets the block number for a tokenId when there is an ownership change
    mapping(uint256 => uint256) public ownershipChange;

Within the VotingEscrow#_transferFrom(), the current block number (block.number) would be stored into the ownershipChange storage. According to the inline comment of the VotingEscrow#_transferFrom(), this is done because of the Flash token protection like this:

Set the block of ownership transfer (for Flash token protection)

https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L942-L943

Within the VotingEscrow#balanceOfToken(), the block number of the _tokenId of veALCX, which is stored in the ownershipChange storage, would be checked. If the VotingEscrow#balanceOfToken() would be called in the same block with the veALCX token transfer (VotingEscrow#_transferFrom()), 0 would be returned. This reason is to prevent buying and voting in the same block and perform a Flash token protection (as the inline comment explains in the VotingEscrow#_transferFrom() above). Then, the VotingEscrow#_balanceOfTokenAt() would internally be invoked like this: https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L367-L368

The VotingEscrow#getVotes() would be used to count the voting power of a given account. Within the VotingEscrow#getVotes(), the VotingEscrow#_balanceOfTokenAt() would internally be invoked like this: https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L274

Within the VotingEscrow#getVotes() above, it is supposed to be checked whether or not the VotingEscrow#getVotes() is not called in the same block with the veALCX token transfer (VotingEscrow#_transferFrom()) before the VotingEscrow#_balanceOfTokenAt() is internally invoked (like the VotingEscrow#balanceOfToken()) for a Flash token protection.

However, within the VotingEscrow#getVotes() above, there is no validation to check whether or not the VotingEscrow#getVotes() is not called in the same block with the veALCX token transfer (VotingEscrow#_transferFrom()) before the VotingEscrow#_balanceOfTokenAt() is internally invoked.

This is problematic. Because, within the VotingEscrow#getVotes() above, the VotingEscrow#_balanceOfTokenAt() will internally be invoked with each tokenId (tId) of veALCX respectively in the for-loop. Since there is no validation before the VotingEscrow#_balanceOfTokenAt() is called, the voting power for these all tokenIds (tlds) of veALCX will be calculated as normal - even if the VotingEscrow#getVotes() would be called in the same block with the veALCX token transfer (VotingEscrow#_transferFrom()).

Hence, there will be no Flash token protection - if the voting mechanism integration is made with the VotingEscrow#getVotes().

This lead to potentially allowing a malicious actor to launch a Flash token attack to manipulate the governance voting result.

NOTE: I acknowledge this is a view function and the impact is limited within the current codebase in the scope. However, I assume that the VotingEscrow#getVotes() will be used for actual voting and user voting powers will be determined based on that. I submitted it as medium severity even though voting manipulations would normally be high because of that assumption.

Impact Details

This allow a malicious actor to launch a Flash token attack by utilizing the VotingEscrow#getVotes() - if the voting mechanism integration is made with the VotingEscrow#getVotes().

PoC

References

  • https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L942-L943

  • https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L367-L368

  • https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/VotingEscrow.sol#L274

Recommendation

Within the VotingEscrow#getVotes(), consider implementing the validation to check whether or not the VotingEscrow#getVotes() is not called in the same block with the veALCX token transfer (VotingEscrow#_transferFrom()) before the VotingEscrow#_balanceOfTokenAt() is internally invoked like this:

Proof of Concept

Last updated

Was this helpful?