functionclaim(uint256 tokenId,address token,address alchemist,uint256 amount,address recipient ) externaloverride {// ...if (alchemists[alchemist] !=address(0)) {require(token ==IAlchemistV2(alchemist).debtToken(),"Invalid alchemist/alchemic-token pair"); (,address[] memory deposits) =IAlchemistV2(alchemist).accounts(recipient);IERC20(token).approve(alchemist, amount); // <==== Audit// Only burn if there are deposits amountBurned = deposits.length >0?IAlchemistV2(alchemist).burn(amount, recipient) :0; }// ... }
Bribe@getRewardForOwner writes the same checkpoint in a loop
Issue Description
When multiple tokens are claimed, the same check point (same balance, same timestamp) will be written multiple times (up to the number of claimed tokens).
Rewards will be lost for a token if it's merged before claiming them
Issue Description
VotingEscrow@merge doesn't claim rewards from RewardsDistributor for the merged token before burning it. This makes it impossible to claim the rewards if it wasn't done before merging since the approval check will fail.
functionmerge(uint256_from,uint256_to) external {require(!voted[_from],"voting in progress for token");require(_from != _to,"must be different tokens");require(_isApprovedOrOwner(msg.sender, _from),"not approved or owner");require(_isApprovedOrOwner(msg.sender, _to),"not approved or owner"); LockedBalance memory _locked0 = locked[_from]; LockedBalance memory _locked1 = locked[_to];// Cannot merge if cooldown is active or lock is expiredrequire(_locked0.cooldown ==0,"Cannot merge when cooldown period in progress");require(_locked1.cooldown ==0,"Cannot merge when cooldown period in progress");require(_locked0.end > block.timestamp,"Cannot merge when lock expired");require(_locked1.end > block.timestamp,"Cannot merge when lock expired");uint256 value0 =uint256(_locked0.amount);// If max lock is enabled retain the max lock _locked1.maxLockEnabled = _locked0.maxLockEnabled ? _locked0.maxLockEnabled : _locked1.maxLockEnabled;IFluxToken(FLUX).mergeFlux(_from, _to);// If max lock is enabled end is the max lock time, otherwise it is the greater of the two end timesuint256 end = _locked1.maxLockEnabled? ((block.timestamp + MAXTIME) / WEEK) * WEEK: _locked0.end >= _locked1.end? _locked0.end: _locked1.end; locked[_from] =LockedBalance(0,0,false,0);_checkpoint(_from, _locked0,LockedBalance(0,0,false,0));_burn(_from, value0);_depositFor(_to, value0, end, _locked1.maxLockEnabled, _locked1, DepositType.MERGE_TYPE); }
functionclaim(uint256_tokenId,bool_compound) externalpayablenonReentrantreturns (uint256) {if (!_compound) {require(msg.value ==0,"Value must be 0 if not compounding"); }bool approvedOrOwner =IVotingEscrow(votingEscrow).isApprovedOrOwner(msg.sender, _tokenId);bool isVotingEscrow = msg.sender == votingEscrow;require(approvedOrOwner || isVotingEscrow,"not approved"); // <==== Audit// ... }
Voter@pokeTokens will fail when the poked token lock is expired
Issue Description
It will fail at the reset step if the token already voted in the current epoch, either normally or by the owner front running the pokeTokens transaction with a reset.
It will fail at the poke (poke -> vote) step because the voting power of the token will be 0.
functionpoke(uint256_tokenId) public {// Previous boost will be taken into account with weights being pulled from the votes mappinguint256 _boost =0;// ...address[] memory _poolVote = poolVote[_tokenId];uint256 _poolCnt = _poolVote.length;uint256[] memory _weights =newuint256[](_poolCnt);for (uint256 i =0; i < _poolCnt; i++) { _weights[i] = votes[_tokenId][_poolVote[i]]; }_vote(_tokenId, _poolVote, _weights, _boost); // <==== Audit }function_vote(uint256_tokenId,address[] memory_poolVote,uint256[] memory_weights,uint256_boost) internal {_reset(_tokenId);uint256 _poolCnt = _poolVote.length;uint256 _totalVoteWeight =0;uint256 _totalWeight =0;for (uint256 i =0; i < _poolCnt; i++) { _totalVoteWeight += _weights[i]; // <==== Audit }IFluxToken(FLUX).accrueFlux(_tokenId);uint256 totalPower = (IVotingEscrow(veALCX).balanceOfToken(_tokenId) + _boost); // <==== Auditfor (uint256 i =0; i < _poolCnt; i++) {address _pool = _poolVote[i];address _gauge = gauges[_pool];require(isAlive[_gauge],"cannot vote for dead gauge");uint256 _poolWeight = (_weights[i] * totalPower) / _totalVoteWeight; // <==== Audit// ... votes[_tokenId][_pool] += _poolWeight;// ... }// ... }
There is no link between the Minter and Voter epochs
Issue Description
There is no link between the Minter and Voter epochs even if they share the same duration.
This means that voters can vote as soon as possible even before a distribution.
In this case, total votes will be reset to 0 in bribes discarding previous voters and leading to the rewards being split based on future voters but still be distributed to all of them. Not all of them will able to claim though (fewer voters are accounted for mean more rewards per voter).
The proposer threshold (amount needed to be able to create a proposal) is checked at the proposal creation time based on the veALCX balance of the creator and the veALCX total supply at that time :
This would allow to create a proposal with 0 voting power as the proposal threshold is initially 0 when veALCX total voting power is still 0 (or decays to 0). This may also lead to easily pass a proposal depending on the veALCX voting power created up to the start of the voting period.
Reentrancy guard modifier is named differently across different contracts