31470 - [SC - Critical] Bribing protocols pay bribes but dont get emiss...

Submitted on May 20th 2024 at 02:10:23 UTC by @savi0ur for Boost | Alchemix

Report ID: #31470

Report type: Smart Contract

Report severity: Critical

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

Impacts:

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

Description

Bug Description

Voters can vote for a particular pools in gauges only when new epoch start. Bribes are awarded based on the voting power at EPOCH_END - 1

https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Voter.sol#L228-L249

function vote(
    uint256 _tokenId,
    address[] calldata _poolVote,
    uint256[] calldata _weights,
    uint256 _boost
) external onlyNewEpoch(_tokenId) {
    require(IVotingEscrow(veALCX).isApprovedOrOwner(msg.sender, _tokenId), "not approved or owner");
    require(_poolVote.length == _weights.length, "pool vote and weights mismatch");
    require(_poolVote.length > 0, "no pools voted for");
    require(_poolVote.length <= pools.length, "invalid pools");
    require(
        IVotingEscrow(veALCX).claimableFlux(_tokenId) + IFluxToken(FLUX).getUnclaimedFlux(_tokenId) >= _boost,
        "insufficient FLUX to boost"
    );
    require(
        (IVotingEscrow(veALCX).balanceOfToken(_tokenId) + _boost) <= maxVotingPower(_tokenId),
        "cannot exceed max boost"
    );
    require(block.timestamp < IVotingEscrow(veALCX).lockEnd(_tokenId), "cannot vote with expired token");

    _vote(_tokenId, _poolVote, _weights, _boost);
}

https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Voter.sol#L105-L109

https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Voter.sol#L412-L455

Actual distribution of emissions to gauges happens after EPOCH_END + X. There is a delay between the end of the epoch and when the rewards are distributed to the gauges. Although there could be a keeper bot which will be sending distribute() tx right at the start of next epoch, but this tx can always be frontrun by voters to take advantage from.

https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Voter.sol#L341-L350

https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Voter.sol#L359-L380

As we can see in distribute(), rewards (stored in claimable[gauge]) from previous epoch are sent to the gauge and its then cleared. Later _updateFor(gauge) is called to update the claimable[gauge] mapping with any accrued reward (reward emission) from previous epoch. This updated rewards in claimable can only be claimed in the next epoch.

https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Minter.sol#L134-L187

Once the reward distribution over, distribution of emission to gauge will be executed using Minter.updatePeriod(). Those emitted rewards are sent to Voter contract.

Things to note here is that

  • Bribes will be awarded based on voting power at EPOCH_END - 1, but Voter.distribute() can be called at a time after EPOCH_END.

  • Some voters may switch their votes before their votes influences emission, causing voter to receive bribes, but bribing protocol to not have received their gauge emissions.

Attack Scenario:

Lets say some Protocol X wants to bribe for an epoch, expecting reward emission for their Gauge A.

  • Epoch 1: Voter votes for Gauge A and earns bribes.

  • End of Epoch 1 (EPOCH_END - 1): Voter's vote is still for Gauge A, bribes calculated.

  • After EPOCH_END + X: Voter switches vote to Gauge B and then allowing distribution of rewards to gauges. This is possible by frontrunning distribute transaction.

  • Result: Voter receives bribes intended for Gauge A of an epoch, but emissions are now directed to Gauge B instead of Gauge A, causing an imbalance. This means that Gauge A does not receive the emissions it was supposed to get based on the bribes it paid for, while the voter still collects the bribes.

Impact

The voter can effectively "double dip" by getting bribes from Gauge A and then directing the actual emissions (rewards) to Gauge B, potentially exploiting the system to maximize their gains. Bribing protocols expect their bribes to translate into emissions for their gauges. This manipulation disrupts that expectation. Due to this, there is a risk of losing such partners.

Recommendation

It should have some small window of say 1 hours at the start of each epoch to prevent any vote switching and allowing keeper bot to distribute rewards before any vote flipping could happened.

References

  • https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Voter.sol#L228-L249

  • https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Voter.sol#L105-L109

  • https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Voter.sol#L412-L455

  • https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Voter.sol#L341-L350

  • https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Voter.sol#L359-L380

  • https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Minter.sol#L134-L187

Proof Of Concept

To show difference between two scenarios mentioned below, we have written two test cases, namely: testStealingEmittedRewardsBugDistributeBeforeVote() and testStealingEmittedRewardsBugVoteBeforeDistribute().

  • vote before distribute (Incorrect Order)

  • distribute before vote (Correct Order)

Steps to Run using Foundry:

  • Paste following foundry code in src/test/Voting.t.sol

  • Both mentioned test cases cab be run using FOUNDRY_PROFILE=default forge test --fork-url $FORK_URL --fork-block-number 17133822 --match-contract VotingTest --match-test testStealingEmittedRewardsBug -vv

Console Output:

Last updated

Was this helpful?