31584 - [SC - Critical] Loss Of Boosted Weight When Poking In The Same ...

Submitted on May 21st 2024 at 14:48:26 UTC by @Limbooo for Boost | Alchemix

Report ID: #31584

Report type: Smart Contract

Report severity: Critical

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

Impacts:

  • Permanent freezing of unclaimed yield

Description

Brief/Intro

The Voter contract has an issue where users lose their boosted voting weight if they "poke" their vote within the same boosted vote period. This leads to a loss of boosted voting power and can potentially result in unclaimed bribe rewards being permanently frozen in the contract.

Vulnerability Details

The poke function is intended to update the user's voting weight. However, it resets the boost to zero when it re-calculates the vote, as seen in the following snippet:

src/Voter.sol:
  194:     /// @inheritdoc IVoter
  195:     function poke(uint256 _tokenId) public {
  196:         // Previous boost will be taken into account with weights being pulled from the votes mapping
@>197:         uint256 _boost = 0;
  ....
  205:         uint256[] memory _weights = new uint256[](_poolCnt);
  206: 
  207:         for (uint256 i = 0; i < _poolCnt; i++) {
  208:             _weights[i] = votes[_tokenId][_poolVote[i]];
  209:         }
  210: 
@>211:         _vote(_tokenId, _poolVote, _weights, _boost);
  212:     }


  412:     function _vote(uint256 _tokenId, address[] memory _poolVote, uint256[] memory _weights, uint256 _boost) internal {
  413:         _reset(_tokenId);
  ....
  419:         for (uint256 i = 0; i < _poolCnt; i++) {
  420:             _totalVoteWeight += _weights[i];
  421:         }
  422: 
  423:         IFluxToken(FLUX).accrueFlux(_tokenId);
@>424:         uint256 totalPower = (IVotingEscrow(veALCX).balanceOfToken(_tokenId) + _boost);
  425: 
  426:         for (uint256 i = 0; i < _poolCnt; i++) {
  427:             address _pool = _poolVote[i];
  428:             address _gauge = gauges[_pool];
  429: 
  430:             require(isAlive[_gauge], "cannot vote for dead gauge");
  431: 
@>432:             uint256 _poolWeight = (_weights[i] * totalPower) / _totalVoteWeight;
  ....
  439:             weights[_pool] += _poolWeight;
  440:             votes[_tokenId][_pool] += _poolWeight;
  441:             IBribe(bribes[_gauge]).deposit(uint256(_poolWeight), _tokenId);
  442:             _totalWeight += _poolWeight;
  443:             emit Voted(msg.sender, _pool, _tokenId, _poolWeight);
  444:         }
  ....
  455:     }

When the poke function is called (which users may interact with when they want to update the voting power after depositing more token into veALCX), it sets _boost to 0 before calling _vote, leading to a recalculation of voting power without considering the previously accumulated boost (see line 424).

Additionally, the pokeTokens function, which can be called by the admin, also resets the boost, causing the same issue.

Impact Details

  • Loss of Boosted Voting Weight: Users lose their boosted voting power if they poke their vote within the same boosted period, which can lead to a reduction in their voting influence.

  • Potential Unclaimed Yield: The loss of boosted weight can result in unclaimed bribe rewards remaining in the contract.

Mitigation Analysis

To mitigate this issue, the poke function should be updated to correctly account for the accumulated boost. The _boost variable should be calculated and passed appropriately to the _vote function. Since boost is only valid for the epoch it was used in, ensure that this information is properly retained and used when recalculating votes within the same epoch.

Proof of Concept

The test can be added to a new file under the current test suite src/test/VotingPoC.t.sol, then specify the file name in FILE flag under Makefile configuration. Run using make test_file

Last updated

Was this helpful?