31377 - [SC - Critical] Stucked yield tokens upon withdrawal of votes f...

Submitted on May 17th 2024 at 18:16:22 UTC by @Saediek for Boost | Alchemix

Report ID: #31377

Report type: Smart Contract

Report severity: Critical

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

Impacts:

  • Permanent freezing of unclaimed yield

Description

Brief/Intro

The withdraw() method in the Bribe contract doesn't update the totalVoting if a staker/voter decides to withdraw his/her stake this would lead to a scenario where a percentage of rewardToken meant for the staker that withdraws his/her tokens gets stucked in the pool instead of distributing it among other participants.

Vulnerability Details

Stucked rewards tokens upon withdrawal of votes from Bribe contract. The Bribe module is an essential part of the whole system it mainly functions as a distributor for reward tokens to voters for a particular guage and it accounts for votes per epoch,reward tokens per epoch among so many other things. In the CONTRACT.md it says and i quote "the total amount of bribe b on pool p claimable by a veALCX NFT with token-ID i during a given epoch n is equal to the proportion of total veALCX power that that NFT used to vote on pool `p",which translates to BribePerNFT(n)=(NFTVoteAmount(n)*REWARDS)/TotalVotes(n)(where BribePerNFT refers to the amount of bribe a tokenId is expected to receive).This equation doesn't always hold and i would prove it to you with an illustration.Lets say we have 5 entities or voters(A,B,C,D,E) at the current epoch they all hold 20 votes each and all of them wagered their votes on the guage so according to the Guage Bribe the totalVotes =100 and each entity has a 20% stake on the rewardTokens for that epoch but towards the end of that epoch A decides to withdraw from the votes so there are four stakers left which implies that each staker should be entitled to 25% of the totalRewards for that epoch,but this isn't true because whenever a withdrawal occurs the totalVote isn't deducted and the bribes Owed to a tokenId=votes/totalVotes *rewardTokens which means the bribes owed to each entity remains 20% each and since there are 4 entities left it sums up to 80% and the remaining 20% remains unclaimable and stuck in the pool forever and ever.

The bug is spotted in the deposit method: whenever a deposit is made the totalVoting variable is increased and whenever a withdrawal is made the totalVoting variable should also be decreased. ##Code Snippet function deposit(uint256 amount, uint256 tokenId) external { require(msg.sender == voter);

    totalSupply += amount;
    balanceOf[tokenId] += amount;

    totalVoting += amount;

    _writeCheckpoint(tokenId, balanceOf[tokenId]);
    _writeSupplyCheckpoint();
    _writeVotingCheckpoint();

    emit Deposit(msg.sender, tokenId, amount);
}

function withdraw(uint256 amount, uint256 tokenId) external { require(msg.sender == voter);

but the totalVoting isn't decreased in withdraw() which means totalVoting before and after withdrawal would always stay the same but since the newVoting power is less than totalVoting the portion of rewards previously entitled to the user who withdrew his/her tokens are stucked in the pool.

Impact Details

The ripple effect of this issue is that a portion of reward tokens whenever a withdrawal occurs would be lost in the bribe contract forever.

References

Withdraw():[https://github.com/alchemix-finance/alchemix-v2-dao/blob/f1007439ad3a32e412468c4c42f62f676822dc1f/src/Bribe.sol#L319] ##Recommendation: Modify the withdraw method from : function withdraw(uint256 amount, uint256 tokenId) external { require(msg.sender == voter);

to: function withdraw(uint256 amount, uint256 tokenId) external { require(msg.sender == voter);

And also they should be a recovery mode for cases where all the voters withdraw their votes so that rewardTokens wouldn't just get lost in the pool.

Proof of Concept

//SPDX-License-Identifier:UNLICENSED //@author:[email protected] /**

  • Steps to run file

  • Create a file called {file}.t.sol in the test directory of the alchemix-v2-dao directory

  • Paste code in the newly created file

  • create a remappings.txt in your root directory.

  • paste the following

  • foundry-libs/=alchemix/lib/forge-std/src

  • alchemix/=src/

  • openzeppelin/=lib/openzeppelin-contracts/ */ pragma solidity ^0.8; import "foundry-libs/Test.sol"; import { Bribe } from "alchemix/Bribe.sol"; import "openzeppelin/contracts/token/ERC20/ERC20.sol";

contract BribeTest is Test { /** *Addres of the users or voters */

}

contract MockVoter { address public veALCX;

} //Mock contract for the veALCX contract to represent the owner of token by the address contract veALCX { function ownerOf(uint256 _tokenId) external view returns (address) { return address(uint160(_tokenId)); } } //Mock ERC20 contract which serves as reward tokens contract MockRewardToken is ERC20 { constructor() ERC20("MOCK-TOKEN", "MCK") {}

}

Last updated

Was this helpful?