31198 - [SC - Critical] VotingEscrowmerge does not check whether the _f...

Submitted on May 14th 2024 at 20:31:50 UTC by @yttriumzz for Boost | Alchemix

Report ID: #31198

Report type: Smart Contract

Report severity: Critical

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

Impacts:

  • Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield

Description

Brief/Intro

The VotingEscrow.merge interface can merge two $veToken. It checks voted[_from] to ensure that _from $veToken has not voted in the current epoch. However, this check is not comprehensive enough because the user can call Voter.reset to claim $FLUX without setting voted[_from] to true.

Vulnerability Details

Please see the following code. Users can call Voter.reset to receive $FLUX. This interface will call VotingEscrow.abstain to set voted[_tokenId] to false. Moreover, the onlyNewEpoch modifier limits each _tokenId to only call the interface once per epoch. Next we use VotingEscrow.merge to bypass this limit.

///// https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Voter.sol#L183-L192
    function reset(uint256 _tokenId) public onlyNewEpoch(_tokenId) {
        if (msg.sender != admin) {
            require(IVotingEscrow(veALCX).isApprovedOrOwner(msg.sender, _tokenId), "not approved or owner");
        }

        lastVoted[_tokenId] = block.timestamp;
        _reset(_tokenId);
        IVotingEscrow(veALCX).abstain(_tokenId);
        IFluxToken(FLUX).accrueFlux(_tokenId);
    }

Please look at the following code. The VotingEscrow.merge interface can merge two $veToken into one. It only checks voted[_from] but not voter.lastVoted(_from). In other words, we first call Voter.reset(_from) and then merge _from $veToken into another $veToken to continue receiving $FLUX.

This is a brief description of the attack. Please see the PoC code for details.

  1. The attacker owns $veTokenA and calls Voter.reset on $veTokenA. The attacker will receive $FLUX once

  2. The attacker creates a new $veTokenTemp worth 1 wei of $BPT. 1 wei $BPT cost is ~0

  3. The attacker merges $veTokenA into $veTokenTemp

  4. Now treat $veTokenTemp as $veTokenA and go back to the step1

Repeat the above steps to receive unlimited $FLUX

Suggested fix

Check whether the _from $veToken has voted

Impact Details

Attackers can receive unlimited $FLUX unlimitedly

References

None

Proof of Concept

The PoC patch

Run the PoC

The log

Last updated

Was this helpful?