Submitted on May 21st 2024 at 00:31:49 UTC by @gladiator111 for Boost | Alchemix
Report ID: #31519
Report type: Smart Contract
Report severity: Low
Target: https://github.com/alchemix-finance/alchemix-v2-dao/blob/main/src/Voter.sol
Impacts:
- Permanent freezing of unclaimed yield
There is no revert statement in Voter.sol::poke
for first time callers resulting in loss of yield for the epoch
Note - For Immunefi Triage - Do not close this , if you have any doubt then directly ask me through comments, as the boost period is ending it would not be great for this to get closed. It is a legitimate issue, I request you to read the report and POC carefully. Thanks!
The Voter.sol::poke
function doesn't revert when _poolCnt = 0 (_poolVote.length = 0), i.e for first time voters.
function poke(uint256 _tokenId) public {
// Previous boost will be taken into account with weights being pulled from the votes mapping
uint256 _boost = 0;
if (msg.sender != admin) {
require(IVotingEscrow(veALCX).isApprovedOrOwner(msg.sender, _tokenId), "not approved or owner");
}
address[] memory _poolVote = poolVote[_tokenId];
@> uint256 _poolCnt = _poolVote.length; //Ideally should revert if 0
uint256[] memory _weights = new uint256[](_poolCnt);
for (uint256 i = 0; i < _poolCnt; i++) {
_weights[i] = votes[_tokenId][_poolVote[i]];
}
_vote(_tokenId, _poolVote, _weights, _boost);
}
So, if a first time voter calls poke function then he will be marked as voted without even voting for anyone because of empty poolVote[_tokenId]
array. This will prevent him from getting any yield. The situation becomes even worse because he can't even call vote function also because of onlyNewEpoch modifier.
function vote(
uint256 _tokenId,
address[] calldata _poolVote,
uint256[] calldata _weights,
uint256 _boost
) external onlyNewEpoch(_tokenId) { //onlyNewEpoch modifier restricts the user from using this function
He will only be able to vote in the next epoch and all his yield will be lost permanently. This can easily be prevented with a revert statement.
User will lose his yield for the entire epoch.
Modify the function as follows
function poke(uint256 _tokenId) public {
// Previous boost will be taken into account with weights being pulled from the votes mapping
uint256 _boost = 0;
if (msg.sender != admin) {
require(IVotingEscrow(veALCX).isApprovedOrOwner(msg.sender, _tokenId), "not approved or owner");
}
address[] memory _poolVote = poolVote[_tokenId];
uint256 _poolCnt = _poolVote.length;
+ require(_poolCnt > 0, "Vote first" );
uint256[] memory _weights = new uint256[](_poolCnt);
for (uint256 i = 0; i < _poolCnt; i++) {
_weights[i] = votes[_tokenId][_poolVote[i]];
}
_vote(_tokenId, _poolVote, _weights, _boost);
}
Paste the following code in Voting.t.sol
and run the test with the following command
forge test --match-test testYieldFreeze -vvvv --fork-url $FORK_URL
function testYieldFreeze() public {
uint256 tokenId1 = createVeAlcx(admin, TOKEN_1, MAXTIME, false);
address bribeAddress1 = voter.bribes(address(sushiGauge));
address bribeAddress2 = voter.bribes(address(balancerGauge));
createThirdPartyBribe(bribeAddress1, bal, TOKEN_100K);
createThirdPartyBribe(bribeAddress1, aura, TOKEN_100K);
createThirdPartyBribe(bribeAddress2, bal, TOKEN_100K);
createThirdPartyBribe(bribeAddress2, aura, TOKEN_100K);
address[] memory pools = new address[](2);
pools[0] = sushiPoolAddress;
pools[1] = balancerPoolAddress;
uint256[] memory weights = new uint256[](2);
weights[0] = 5000;
weights[1] = 5000;
address[] memory bribes = new address[](2);
bribes[0] = address(bribeAddress1);
bribes[1] = address(bribeAddress2);
address[][] memory tokens = new address[][](1);
tokens[0] = new address[](2);
tokens[0][0] = bal;
tokens[0][1] = aura;
hevm.prank(admin);
voter.poke(tokenId1); // using poke first (ideally should revert but doesn't revert)
hevm.prank(admin);
vm.expectRevert("TOKEN_ALREADY_VOTED_THIS_EPOCH"); //can't vote
voter.vote(tokenId1, pools, weights, 0);
hevm.warp(block.timestamp + nextEpoch);
hevm.prank(admin);
vm.expectRevert("no rewards to claim"); // can't claim yield
voter.claimBribes(bribes, tokens, tokenId1);